【GO】七、架构基础与 GORM 简要介绍

架构分析

单体应用的部署架构:
【GO】七、架构基础与 GORM 简要介绍_第1张图片
这种单体应用架构在少部分人开发时,不会产生太多问题,但在项目结构足够大时,就会产生多种需求同时开发的情况,多种需求的同时开发一定会产生先后与master合并的情况,后合并的需求就需要先拉取最新代码再进行回归测试,(无论是否产生代码冲突都必须进行回归测试)在这种情况下就极大的降低了开发效率

另外,我们在进行各个需求开发时,操作同一个数据库,有很大的可能性出现操作同一张表导致一个需求的改动,导致另一个历史模块出现问题,这就是由于模块间隔离性太差导致的,所以在微服务中,考虑对每一个微服务模块创建一个数据库,独立模块进行独立操作,只对外暴露端口,不允许其他模块直接操作数据自己模块的数据库,保证数据隔离性,并引入 MQ 进行解耦,来让整体系统更加成熟。

而,我们模块之间的通信使用 http 协议就显得有些臃肿,效率偏低,我们选择使用rpc协议,来让我们的模块间沟通效率变得更高。

但此时,我们对外需要暴露http请求,对内需要暴露rpc请求,这种情况就要求我们有一种简单的整合方式,这种整合方式就是微服务的分层架构:

【GO】七、架构基础与 GORM 简要介绍_第2张图片
注册中心的理解

然而,当我们由单体架构演变到微服务架构时,我们需要维护许多数据,最典型的就是众多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

ORM概述

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'"` //	指定列名和数据库的数据类型同时设置索引
}

ORM 操作

插入操作

创建操作以及其返回值:

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) 来查找所有的元素,包括被软删除了的数据

原生SQL

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)
	}

你可能感兴趣的:(Go,golang,架构,开发语言)