单体应用的部署架构:
这种单体应用架构在少部分人开发时,不会产生太多问题,但在项目结构足够大时,就会产生多种需求同时开发的情况,多种需求的同时开发一定会产生先后与master合并的情况,后合并的需求就需要先拉取最新代码再进行回归测试,(无论是否产生代码冲突都必须进行回归测试)在这种情况下就极大的降低了开发效率
另外,我们在进行各个需求开发时,操作同一个数据库,有很大的可能性出现操作同一张表导致一个需求的改动,导致另一个历史模块出现问题,这就是由于模块间隔离性太差导致的,所以在微服务中,考虑对每一个微服务模块创建一个数据库,独立模块进行独立操作,只对外暴露端口,不允许其他模块直接操作数据自己模块的数据库,保证数据隔离性,并引入 MQ 进行解耦,来让整体系统更加成熟。
而,我们模块之间的通信使用 http 协议就显得有些臃肿,效率偏低,我们选择使用rpc协议,来让我们的模块间沟通效率变得更高。
但此时,我们对外需要暴露http请求,对内需要暴露rpc请求,这种情况就要求我们有一种简单的整合方式,这种整合方式就是微服务的分层架构:
然而,当我们由单体架构演变到微服务架构时,我们需要维护许多数据,最典型的就是众多web模块所需要的ip地址与端口号,以及他们的存活状态,因为如果因为各种原因导致服务器出现问题,则会出现许多难以预测的问题
我们进入微服务解决方案时,会出现解决 ip 过多、端口号过多、服务存活状态难以检测的问题,这时就产生了注册中心帮助我们解决这个问题
我们将一个个的微服务都注册进注册中心,此时,如果我们需要调用某一个模块的服务,就可以直接在注册中心提供我们自己定义的名称。若服务存活,注册中心会返回给我们其对应的 ip 和 端口号,此处就解决了我们自己记录 ip和端口号的问题。
另外的,如果我们对某些模块采用了集群,服务的存活状态对于整个项目就更重要了,而注册中心可以帮助我们解决这个问题。
同时,当模块变得很多时,我们再进行配置的修改时就会变得麻烦,且我们修改配置之后需要重新启动或部署,而我们使用配置中心进行操作的话,最直接的好处就是我们不需要重新部署,因为服务的配置是从配置中心拉取的。
网关
项目在调用web层接口的时候,同样具有很多的模块,其都有对应的ip和端口号,直接记住这些ip和端口号也是很复杂的,此时网关就应运而生了,网关可以根据你的url的不同将请求路由到不同的模块,当然如果仅仅是这样,Nginx也可以做到相同的功能,但网关比Nginx更强大的地方就在于其充分支持了集群模块,当集群中某一个模块挂掉的时候,网关可以及时发现并将其路由到存活的节点,但Nginx的配置会更加的复杂,另外的,网关还具有鉴权、熔断、ip黑白名单、负载均衡等能力
在前后端分离项目的开发过程中,会产生很多很多的接口,对这些接口需要进行管理,否则项目将杂乱无章,难以维护。
首先要理解前后端分离项目的产生原因:
在传统项目中,前端将 html / css / js 组成的文件传递给后端,后端打包并生成模版渲染之后再返回给浏览器,在这种环境下,一旦前端进行了修改,就必须将前端文件重新打包传递给后端,这时非常繁杂的流程,此时就产生了前后端分离的开发技术。
在前后端分离的开发技术中,后端只负责向前端传递所需要的数据,即一个 json / protobuf / xml ,前端解析这个数据并将其渲染到页面上,注意此时,这个前端服务器是需要独立部署的,其和后端服务器,可以占用同一台机器的不同端口,也可以直接占用两台不同的机器。
另外的,前端需要请求许多许多的后端接口,这些接口就需要文档进行维护,swagger就是具有类似功能的技术,前端使用mock技术来模仿后端接口,后端使用类似技术(如Postman)来模仿http请求,在这个过程中,前后端的接口是各自独立的,这十分不利于接口文档的整体维护,因为一旦涉及到修改,需要在前后端人为修改,很容易造成错误,且极大造成浪费,接口 API 管理工具有许多可选,选用自己习惯的即可。
ORM 全称为 Object Relation Mapping(对象关系映射),主要作用就是在编程中,将每张表作为一个对象,将 对象 - 表 的关系进行一一映射,
这里介绍 gorm 作为 go 语言的 orm 框架。
ORM的优点:大幅度提高开发效率
ORM的缺点:屏蔽了SQL 的细节,时间长了会阻碍对于SQL 的理解
其转换成SQL的过程也降低了性能
数据库的创建:
字符集:utf8mb4
排序规则:utf8mb4_general_ci
一个配置数据库并添加日志级别的示例:
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:你的密码@tcp(192.168.202.138:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
// 添加日志信息
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Silent, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: false, // Disable color
},
)
_, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
// 将日添加到配置中
Logger: newLogger,
})
if err != nil {
panic(err)
}
下面是一些基础操作:
// 以结构体创建表
type Product struct {
gorm.Model
Code string
Price uint
}
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:VBwH9eW*urHb@tcp(192.168.202.138:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
// 添加日志信息
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: true, // Disable color,true is colorful, false to black and white
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
// 将日添加到配置中
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 自动生成表结构
//_ = db.AutoMigrate(&Product{})
// 新增操作
//db.Create(&Product{Code: "D42", Price: 100})
// Read
var product Product
db.First(&product, 1) // find product with integer primary key
db.First(&product, "code = ?", "D42") // find product with code D42
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 将product的Price更新为200
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"}) // 将product的Price更新为200 这两个完全一样
//db.Delete(&product, 1) // 删除product,逻辑删除,将DeletedAt字段更新为当前时间
fmt.Println(product.Code)
}
但注意,这里的基础操作不能将数值修改为空值,我们如果有这个需求,就需要进行调整:
package main
import (
"database/sql"
"fmt"
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// 以结构体创建表
type Product struct {
gorm.Model
// 若需要进行空值的调整,则需要将变量定义为这个类型
Code sql.NullString
Price uint
}
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:VBwH9eW*urHb@tcp(192.168.202.138:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
// 添加日志信息
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: true, // Disable color,true is colorful, false to black and white
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
// 将日添加到配置中
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 自动生成表结构
//_ = db.AutoMigrate(&Product{})
// 新增操作
//db.Create(&Product{Code: "D42", Price: 100})
// Read
var product Product
db.First(&product, 1) // find product with integer primary key
db.First(&product, "code = ?", "D42") // find product with code D42
db.Model(&product).Updates(Product{Price: 200, Code: sql.NullString{"", true}}) // 将product的Price更新为200
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"}) // 将product的Price更新为200 这两个完全一样
//db.Delete(&product, 1) // 删除product,逻辑删除,将DeletedAt字段更新为当前时间
fmt.Println(product.Code)
}
注意,对于连接到 ORM 的 struct 是会默认实现 Scanner 和 Valuer 的两个接口
// 以结构体创建表
type Product struct {
gorm.Model
// 若需要进行空值的调整,则需要将变量定义为这个类型
Code sql.NullString
Price uint
}
例如上面这个 gorm.Model 其中就包括下面:
// gorm.Model
type Model struct{
// 标记主键
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.TIme
DeletedAt gorm.DeletedAt
}
例如这个示例里面,可以使用 `` 标签来标记约束,例如这里的primarykey 就是主键(不区分大小写)
多个约束:
// 以结构体创建表
type Product struct {
UserID uint `gorm:"primarykey"` // 设置为主键,驼峰会被修改为下划线
Name string `gorm:"column:user_name;type:varchar(50);index:idx_user_name;default:'default_value'"` // 指定列名和数据库的数据类型同时设置索引
}
创建操作以及其返回值:
package main
import (
"database/sql"
"fmt"
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// 以结构体创建表
type User struct {
ID uint
Name string
Email *string // 使用指针和sql.NullString都可以解决空值无法被赋值的问题
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
}
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:VBwH9eW*urHb@tcp(192.168.202.138:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
// 添加日志信息
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: true, // Disable color,true is colorful, false to black and white
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
// 将日添加到配置中
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 自动生成表结构
_ = db.AutoMigrate(&User{})
user := User{
Name: "Cecilia",
}
result := db.Create(&user) // 此处由于我们的数据类型是指针,所以其会传入 NULL,而不是会像普通数据类型一样不做处理
fmt.Println(user.ID) // 在ORM中,user.ID会被自动赋值为主键
fmt.Println(result.Error) // 错误信息
fmt.Println(result.RowsAffected) // 影响行数
}
var user = []User{{Name: "KeNan"}, {Name: "Lan"}, {Name: "Wu"}}
db.Create(&user) // 批量插入
另外的,在批量插入中,一般会设置批量的条数,这是因为SQL语句是有长度限制的,不加限制会令插入失败
var user = []User{{Name: "KeNan"}, {Name: "Lan"}, {Name: "Wu"}}
db.CreateInBatches(&user, 100) // 批量插入,把数据分成 100 一组,分批插入
我们也可以利用钩子进行权限判断
也可以配合Model进行插入:
// 在表已存在的时候,一般使用下面来进行插入
db.Model(&User{}).Create([]map[string]interface{}{
{"Name": "Lan"},
})
package main
import (
"database/sql"
"errors"
"fmt"
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// 以结构体创建表
type User struct {
ID uint
Name string
Email *string // 使用指针和sql.NullString都可以解决空值无法被赋值的问题
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
}
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:VBwH9eW*urHb@tcp(192.168.202.138:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
// 添加日志信息
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: true, // Disable color,true is colorful, false to black and white
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
// 将日添加到配置中
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 注意查询是按照对象的内容进行查询的,故而不能给对象赋值,否则会按照赋值的内容进行查询,这里仅做演示
var user User
db.First(&user) // 按照主键升序排列获取第一条记录
fmt.Println(user.ID)
db.Take(&user) // 没有排序条件的情况下获取第一条记录
fmt.Println(user.ID)
result := db.Last(&user) // 按照主键降序获取第一条记录
fmt.Println(user.ID)
// 同样的,我们可以获取对应的结果
fmt.Println(result.Error)
fmt.Println(result.RowsAffected) // 这个在查询中返回的是查询到的记录数
// 按照主键查询
db.First(&user, 2)
fmt.Println(user.ID)
// 另外,查询操作可能会发生没有找到记录的情况,这种情况可以按照下面的模式进行判断
Flag := errors.Is(result.Error, gorm.ErrRecordNotFound)
fmt.Println(Flag)
// 查多条
var users []User
db.Find(&users) // 查全部,相当于select * from users
fmt.Println(users)
// 另外,也可以这样来查找部分的记录排序后的第一条记录
var userss User
db.First(&userss, []int{1, 2, 3})
fmt.Println(userss)
}
package main
import (
"database/sql"
"fmt"
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// 以结构体创建表
type User struct {
ID uint
Name string
Email *string // 使用指针和sql.NullString都可以解决空值无法被赋值的问题
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
}
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:VBwH9eW*urHb@tcp(192.168.202.138:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
// 添加日志信息
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: true, // Disable color,true is colorful, false to black and white
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
// 将日添加到配置中
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 条件查询某一条语句
var user User
// 注意这里的 条件 name 必须为数据库中的字段名,而不是结构体中的字段名,另外,其不区分大小写
//db.Where("name = ?", "KeNan").First(&user) // 这里的.First 起到了指定表名以及 limit = 1 的效果
// 若我们想使用字段名,则可以下面这样
db.Where(&User{Name: "Cecilia"}).First(&user)
fmt.Println(user.ID)
var users []User
// 查询多条记录
db.Where("name = ? || name = ?", "KeNan", "Cecilia").Find(&users)
fmt.Println(users)
}
同样的,对于零值,上面这种条件查询若没有在数据类型定义的阶段明确定义的话,零值是不会被拼凑的。
另外的,gorm提供了明确的 HOOK 方法接口,我们可以像实现 AOP 一样快速的在数据库操作的前后进行操作,相当于数据库版的 AOP 操作
// 更新一条语句的简单流程
var user User
db.First(&user)
user.Name = "CeciliaNew"
// 若存在为更新,不存在则插入
db.Save(&user)
// 条件更新
db.Model(&User{}).Where("name = ?", "CeciliaNew").Update("name", "CeciliaNewNew")
使用 db.Delete(&user)进行删除,此处要注意一个问题:若我们的字段中没有带 gorm.DeleteAt,则这里就是物理删除
若有 gorm.DeleteAt,这里就是逻辑删除,其会给 DeleteAt 字段附上删除的时间,若果没删除就是 nil
在有软删除能力时,我们可以使用 db.Unscoped().Find(&users) 来查找所有的元素,包括被软删除了的数据
db.Raw(‘SQL语句’).Scan(&user)
建立两个结构体并进行引用会创建两张表并引入外键
type Company struct {
ID int
Name string
}
// 以结构体创建表
type employee struct {
gorm.Model
Name string
CompanyID int
Company Company // 这里会被视为外键的表,而不是一个字段
}
// 这样在创建一个表时,就会根据其外键的匹配自动创建另一个表,并根据主键和外键创建索引
//db.AutoMigrate(Employee{})
//db.Create(&Employee{
// Name: "Eason",
// Company: Company{ // 这里如果不指定关联外键的ID,则会自动创建一个新的公司
// Name: "Alibaba",
// },
//})
//db.Create(&Employee{
// Name: "JJ",
// Company: Company{
// ID: 1, // 这里指定了外键的ID,所以不会创建新的公司
// },
//})
var emp Employee
db.First(&emp)
fmt.Println(emp.CompanyID)
var emp Employee
//db.First(&emp)
//fmt.Println(emp.Name, emp.Company.ID) // 这里是查不到公司信息的,因为GORM默认不会查询关联表,我们需要其他方法的支持
// 若我们需要查询关联表的信息,则可以使用Preload方法
// 这里会执行两个SQL:SELECT * FROM `companies` WHERE `companies`.`id` = ? 先查询公司信息(根据传入的emp)
//db.Preload("Company").First(&emp)
//fmt.Println(emp.Name, emp.Company.ID, emp.Company.Name)
db.Joins("Company").First(&emp)
fmt.Println(emp.Name, emp.Company.ID, emp.Company.Name)
对于一对多的情况,例如:一个用户有多张信用卡,我们就必须使用一个切片来代表一对多的元素存储在用户中:
注意:由这种情况输出的两张表不会自动创建外键
type User struct {
gorm.Model
Credicards []Credicard
}
type Credicard struct {
gorm.Model
Number string
UserID uint
}
// 创建两张比哦啊哦
//db.AutoMigrate(&User{})
//db.AutoMigrate(&Credicard{})
对于多对多的关系来讲,无法通过建立约束的方式来实现表的关联,故而一般使用建立一张中间表的方式来存储多对多关系的表。
type User3 struct {
gorm.Model
// 创建第三张表,用于关联两张表
Languages []Language `gorm:"many2many:user_languages;"` // 这里的user_languages是第三张表的表名
}
type Language struct {
gorm.Model
Name string
}
添加数据:
languages := []Language{}
languages = append(languages, Language{Name: "GoLang"})
languages = append(languages, Language{Name: "Java"})
user := User3{
Languages: languages,
}
// 这一条语句会向三张表中都插入数据
db.Create(&user)
// 遍历一下:
var user User3
db.Preload("Languages").First(&user)
for _, k := range user.Languages {
fmt.Println(k.Name)
}