Golang微服框架Kratos與它的小伙伴系列 - ORM框架 - GORM
什么是ORM岸裙?
面向?qū)ο缶幊毯完P(guān)系型數(shù)據(jù)庫,都是目前最流行的技術(shù)年叮,但是它們的模型是不一樣的瘸洛。
面向?qū)ο缶幊贪阉袑?shí)體看成對(duì)象(object),關(guān)系型數(shù)據(jù)庫則是采用實(shí)體之間的關(guān)系(relation)連接數(shù)據(jù)。很早就有人提出呆盖,關(guān)系也可以用對(duì)象表達(dá)拖云,這樣的話,就能使用面向?qū)ο缶幊逃τ郑瑏聿僮麝P(guān)系型數(shù)據(jù)庫宙项。
簡(jiǎn)單說,ORM 就是通過實(shí)例對(duì)象的語法株扛,完成關(guān)系型數(shù)據(jù)庫的操作的技術(shù)尤筐,是"對(duì)象-關(guān)系映射"(Object/Relational Mapping) 的縮寫。
ORM 把數(shù)據(jù)庫映射成對(duì)象洞就。
- 數(shù)據(jù)庫的表(table) --> 類(class)
- 記錄(record盆繁,行數(shù)據(jù))--> 對(duì)象(object)
- 字段(field)--> 對(duì)象的屬性(attribute)
舉例來說,下面是一行 SQL 語句旬蟋。
SELECT id, first_name, last_name, phone, birth_date, sex
FROM persons
WHERE id = 10
程序直接運(yùn)行 SQL油昂,操作數(shù)據(jù)庫的寫法如下。
res = db.execSql(sql);
name = res[0]["FIRST_NAME"];
改成 ORM 的寫法如下倾贰。
p = Person.get(10);
name = p.first_name;
一比較就可以發(fā)現(xiàn)冕碟,ORM 使用對(duì)象,封裝了數(shù)據(jù)庫操作匆浙,因此可以不碰 SQL 語言安寺。開發(fā)者只使用面向?qū)ο缶幊蹋c數(shù)據(jù)對(duì)象直接交互首尼,不用關(guān)心底層數(shù)據(jù)庫挑庶。
ORM 有下面這些優(yōu)點(diǎn):
- 數(shù)據(jù)模型都在一個(gè)地方定義,更容易更新和維護(hù)饰恕,也利于重用代碼。
- ORM 有現(xiàn)成的工具井仰,很多功能都可以自動(dòng)完成埋嵌,比如數(shù)據(jù)消毒、預(yù)處理俱恶、事務(wù)等等雹嗦。
- 它迫使你使用 MVC 架構(gòu),ORM 就是天然的 Model合是,最終使代碼更清晰了罪。
- 基于 ORM 的業(yè)務(wù)代碼比較簡(jiǎn)單,代碼量少聪全,語義性好泊藕,容易理解。
- 你不必編寫性能不佳的 SQL难礼。
ORM 也有很突出的缺點(diǎn):
- ORM 庫不是輕量級(jí)工具娃圆,需要花很多精力學(xué)習(xí)和設(shè)置玫锋。
- 對(duì)于復(fù)雜的查詢,ORM 要么是無法表達(dá)讼呢,要么是性能不如原生的 SQL撩鹿。
- ORM 抽象掉了數(shù)據(jù)庫層,開發(fā)者無法了解底層的數(shù)據(jù)庫操作悦屏,也無法定制一些特殊的 SQL节沦。
什么是GORM?
GORM 是基于Go語言實(shí)現(xiàn)的ORM庫础爬,它是Golang目前比較熱門的數(shù)據(jù)庫ORM操作庫甫贯,對(duì)開發(fā)者也比較友好,使用非常方便簡(jiǎn)單幕帆。
最重要的是获搏,它是一個(gè)正經(jīng)的國(guó)產(chǎn)開源庫。支持國(guó)產(chǎn)失乾!
特性
- 全功能 ORM
- 關(guān)聯(lián) (Has One常熙,Has Many,Belongs To碱茁,Many To Many裸卫,多態(tài),單表繼承)
- Create纽竣,Save砍艾,Update,Delete仑撞,F(xiàn)ind 中鉤子方法
- 支持 Preload突倍、Joins 的預(yù)加載
- 事務(wù),嵌套事務(wù)穴吹,Save Point幽勒,Rollback To Saved Point
- Context、預(yù)編譯模式港令、DryRun 模式
- 批量插入啥容,F(xiàn)indInBatches,F(xiàn)ind/Create with Map顷霹,使用 SQL 表達(dá)式咪惠、Context Valuer 進(jìn)行 CRUD
- SQL 構(gòu)建器,Upsert淋淀,數(shù)據(jù)庫鎖遥昧,Optimizer/Index/Comment Hint,命名參數(shù),子查詢
- 復(fù)合主鍵渠鸽,索引叫乌,約束
- Auto Migration
- 自定義 Logger
- 靈活的可擴(kuò)展插件 API:Database Resolver(多數(shù)據(jù)庫,讀寫分離)徽缚、Prometheus…
- 每個(gè)特性都經(jīng)過了測(cè)試的重重考驗(yàn)
- 開發(fā)者友好
安裝庫
go get -u gorm.io/gorm
除此以外憨奸,還需要安裝數(shù)據(jù)庫的驅(qū)動(dòng):
# 安裝SQLite驅(qū)動(dòng)
go get -u gorm.io/driver/sqlite
# 安裝MySQL驅(qū)動(dòng)
go get -u gorm.io/driver/mysql
# 安裝PostgreSQL驅(qū)動(dòng)
go get -u gorm.io/driver/postgres
# 安裝SQL Server驅(qū)動(dòng)
go get -u gorm.io/driver/sqlserver
# 安裝Clickhouse驅(qū)動(dòng)(Clickhouse兼容MySQL的協(xié)議,所以直接用MySQL驅(qū)動(dòng)連接也是一樣的)
go get -u gorm.io/driver/clickhouse
GORM的一些數(shù)據(jù)庫基本操作
因?yàn)閿?shù)據(jù)庫是復(fù)雜的凿试,SQL是復(fù)雜的排宰,復(fù)雜到能夠出好幾本書,所以是絕不可能在簡(jiǎn)單的篇幅里面講完整那婉,只能夠?qū)⒊S玫囊恍┎僮鳎ㄟB接數(shù)據(jù)庫板甘、CURD)拿來舉例講講。
連接數(shù)據(jù)庫
MySQL
import (
"gorm.io/gorm"
"gorm.io/driver/mysql"
)
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
PostgreSQL
import (
"gorm.io/gorm"
"gorm.io/driver/postgres"
)
dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
SQLite
import (
"gorm.io/gorm"
"gorm.io/driver/sqlite"
)
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
SQL Server
import (
"gorm.io/gorm"
"gorm.io/driver/sqlserver"
)
dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
Clickhouse
import (
"gorm.io/gorm"
"gorm.io/driver/clickhouse"
)
dsn := "tcp://localhost:9000?database=gorm&username=gorm&password=gorm&read_timeout=10&write_timeout=20"
db, err := gorm.Open(clickhouse.Open(dsn), &gorm.Config{})
自動(dòng)遷移 Automatic Migration
db.AutoMigrate(&User{})
自動(dòng)遷移功能详炬,會(huì)創(chuàng)建表盐类、缺失的外鍵、約束呛谜、列和索引在跳。
定義模型
type User struct {
gorm.Model
UserName string
NickName string
}
gorm.Model
則是包含了通用的一些字段,比如:id隐岛、創(chuàng)建時(shí)間猫妙、更新時(shí)間、刪除時(shí)間等……
// gorm.Model definition
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
在默認(rèn)的情況下:
- 表名聚凹,將會(huì)被轉(zhuǎn)換為 復(fù)數(shù)形式 以及 蛇形命名法(snake_case)割坠,比如:
User
轉(zhuǎn)換為users
。 - 字段名妒牙,將被轉(zhuǎn)換為 蛇形命名法(snake_case) 字符串彼哼,比如:
UserName
被轉(zhuǎn)換為user_name
。
當(dāng)然湘今,你也可以用column
標(biāo)明字段名的輸出:
type User struct {
gorm.Model
UserName string `gorm:"column:username"`
NickName string `gorm:"column:nickname"`
}
定義TableName()
方法控制表名的輸出:
func (u User) TableName() string {
return "users"
}
增 Create
db.Create(&User{UserName: "TestUserName", NickName: "TestNickName"})
刪 Delete
// 軟刪除
// UPDATE users SET deleted_at="2020-03-13 10:23" WHERE id = user.id;
db.Delete(&user, 1)
db.Delete(&user)
// 批量軟刪除
db.Where("age = ?", 20).Delete(&User{})
// 物理刪除
// DELETE FROM users WHERE id=10;
db.Unscoped().Delete(&user)
改 Update
db.Model(&user).Update("nick_name", "NewNickName")
// Update - 更新多個(gè)字段
db.Model(&user).Updates(User{UserName: "NewUserName", NickName: "NewNickName"})
db.Model(&user).Updates(map[string]interface{}{"user_name": "NewUserName", "nick_name": "NewNickName"})
查 Read
var user User
// 獲取第一條記錄(主鍵升序)
// SELECT * FROM users ORDER BY id LIMIT 1;
db.First(&user)
// 根據(jù)整型主鍵查找
// SELECT * FROM users WHERE id = 10;
db.First(&user, 10)
db.First(&user, "10")
// 根據(jù)主鍵獲取記錄敢朱,如果是非整型主鍵
// SELECT * FROM users WHERE user_name = 'TestUserName';
db.First(&user, "user_name = ?", "TestUserName")
// SELECT * FROM users WHERE id IN (1,2,3);
db.Find(&users, []int{1,2,3})
// 獲取一條記錄,沒有指定排序字段
// SELECT * FROM users LIMIT 1;
db.Take(&user)
// 獲取最后一條記錄(主鍵降序)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
db.Last(&user)
與Kratos攜起手來
官方推薦的包結(jié)構(gòu)是這樣的:
|- data
|- biz
|- service
|- server
那么象浑,我們可以把模型定義做成一個(gè)package蔫饰,放到data文件夾下面去:
|- data
| |- modal
|- biz
|- service
|- server
創(chuàng)建數(shù)據(jù)庫客戶端
在data/data.go
文件中添加創(chuàng)建Gorm數(shù)據(jù)庫客戶端的方法NewGormClient
:
import (
"gorm.io/driver/clickhouse"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
)
// Data .
type Data struct {
db *gorm.Client
}
// NewGormClient 創(chuàng)建數(shù)據(jù)庫客戶端
func NewGormClient(cfg *conf.Bootstrap, logger log.Logger) *gorm.DB {
l := log.NewHelper(log.With(logger, "module", "ent/data/user-service"))
var driver gorm.Dialector
switch cfg.Data.Database.Driver {
default:
fallthrough
case "mysql":
driver = mysql.Open(cfg.Data.Database.Source)
break
case "postgres":
driver = postgres.Open(cfg.Data.Database.Source)
break
case "clickhouse":
driver = clickhouse.Open(cfg.Data.Database.Source)
break
case "sqlite":
driver = sqlite.Open(cfg.Data.Database.Source)
break
case "sqlserver":
driver = sqlserver.Open(cfg.Data.Database.Source)
break
}
client, err := gorm.Open(driver, &gorm.Config{})
if err != nil {
l.Fatalf("failed opening connection to db: %v", err)
}
// 運(yùn)行數(shù)據(jù)庫遷移工具
if cfg.Data.Database.Migrate {
if err := client.AutoMigrate(
&models.User{},
); err != nil {
l.Fatalf("failed creating schema resources: %v", err)
}
}
return client
}
并將之注入到ProviderSet
// ProviderSet is data providers.
var ProviderSet = wire.NewSet(
NewGormClient,
...
)
需要說明的是數(shù)據(jù)庫遷移工具琅豆,如果數(shù)據(jù)庫中不存在表愉豺,遷移工具會(huì)創(chuàng)建一個(gè);如果字段存在改變茫因,遷移工具會(huì)對(duì)字段進(jìn)行修改蚪拦。
創(chuàng)建UseCase
在biz文件夾下創(chuàng)建user.go
:
package biz
type UserRepo interface {
ListUser(ctx context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error)
GetUser(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error)
CreateUser(ctx context.Context, req *v1.CreateUserRequest) (*v1.User, error)
UpdateUser(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error)
DeleteUser(ctx context.Context, req *v1.DeleteUserRequest) (bool, error)
}
type UserUseCase struct {
repo UserRepo
log *log.Helper
}
func NewUserUseCase(repo UserRepo, logger log.Logger) *UserUseCase {
l := log.NewHelper(log.With(logger, "module", "user/usecase"))
return &UserUseCase{repo: repo, log: l}
}
func (uc *UserUseCase) ListUser(ctx context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error) {
return uc.repo.ListUser(ctx, req)
}
func (uc *UserUseCase) GetUser(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error) {
return uc.repo.GetUser(ctx, req)
}
func (uc *UserUseCase) CreateUser(ctx context.Context, req *v1.CreateUserRequest) (*v1.User, error) {
return uc.repo.CreateUser(ctx, req)
}
func (uc *UserUseCase) UpdateUser(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error) {
return uc.repo.UpdateUser(ctx, req)
}
func (uc *UserUseCase) DeleteUser(ctx context.Context, req *v1.DeleteUserRequest) (bool, error) {
return uc.repo.DeleteUser(ctx, req)
}
注入到biz.ProviderSet
package biz
// ProviderSet is biz providers.
var ProviderSet = wire.NewSet(
NewUserUseCase,
...
)
創(chuàng)建Repo
在data
文件夾下創(chuàng)建user.go
文件,實(shí)際操作數(shù)據(jù)庫的操作都在此處。
package data
var _ biz.UserRepo = (*UserRepo)(nil)
type UserRepo struct {
data *Data
log *log.Helper
}
func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo {
l := log.NewHelper(log.With(logger, "module", "User/repo"))
return &UserRepo{
data: data,
log: l,
}
}
func (r *UserRepo) convertModelToProto(in *models.User) *v1.User {
if in == nil {
return nil
}
return &v1.User{
Id: uint32(in.ID),
UserName: &in.UserName,
NickName: &in.NickName,
Password: &in.Password,
CreateTime: util.TimeToTimeString(&in.CreatedAt),
UpdateTime: util.TimeToTimeString(&in.UpdatedAt),
}
}
func (r *UserRepo) List(_ context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error) {
var results []models.User
result := r.data.db.
Limit(int(req.GetPageSize())).
Offset(int(req.GetPageSize() * (req.GetPage() - 1))).
Find(&results)
if result.Error != nil {
return nil, result.Error
}
items := make([]*v1.User, 0, len(results))
for _, res := range results {
item := r.convertModelToProto(&res)
items = append(items, item)
}
var count int64
result = r.data.db.Model(&models.User{}).
Count(&count)
if result.Error != nil {
return nil, result.Error
}
return &v1.ListUserResponse{
Total: int32(count),
Items: items,
}, nil
}
func (r *UserRepo) Get(_ context.Context, req *v1.GetUserRequest) (*v1.User, error) {
res := &models.User{}
r.data.db.First(res, "id = ?", req.GetId())
return r.convertModelToProto(res), nil
}
func (r *UserRepo) Create(_ context.Context, req *v1.CreateUserRequest) (*v1.User, error) {
cryptoPassword, err := crypto.HashPassword(req.User.GetPassword())
if err != nil {
return nil, err
}
res := &models.User{
UserName: req.User.GetUserName(),
NickName: req.User.GetNickName(),
Password: cryptoPassword,
}
result := r.data.db.Create(res)
if result.Error != nil {
return nil, result.Error
}
return r.convertModelToProto(res), err
}
func (r *UserRepo) Update(_ context.Context, req *v1.UpdateUserRequest) (*v1.User, error) {
var cryptoPassword string
var err error
if req.User.Password != nil {
cryptoPassword, err = crypto.HashPassword(req.User.GetPassword())
if err != nil {
return nil, err
}
}
res := &models.User{
UserName: req.User.GetUserName(),
NickName: req.User.GetNickName(),
Password: cryptoPassword,
}
result := r.data.db.Model(res).Updates(res)
if result.Error != nil {
return nil, result.Error
}
return r.convertModelToProto(res), err
}
func (r *UserRepo) Delete(_ context.Context, req *v1.DeleteUserRequest) (bool, error) {
result := r.data.db.Delete(&models.User{}, req.GetId())
if result.Error != nil {
return false, result.Error
}
return true, nil
}
注入到data.ProviderSet
package data
// ProviderSet is data providers.
var ProviderSet = wire.NewSet(
NewUserRepo,
...
)
在Service中調(diào)用
package service
type UserService struct {
v1.UnimplementedUserServiceServer
uc *biz.UserUseCase
log *log.Helper
}
func NewUserService(logger log.Logger, uc *biz.UserUseCase) *UserService {
l := log.NewHelper(log.With(logger, "module", "service/user"))
return &UserService{
log: l,
uc: uc,
}
}
// ListUser 獲取用戶列表
func (s *UserService) ListUser(ctx context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error) {
return s.uc.ListUser(ctx, req)
}
// GetUser 獲取一個(gè)用戶
func (s *UserService) GetUser(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error) {
return s.uc.GetUser(ctx, req)
}
// CreateUser 創(chuàng)建一個(gè)用戶
func (s *UserService) CreateUser(ctx context.Context, req *v1.CreateUserRequest) (*v1.User, error) {
return s.uc.CreateUser(ctx, req)
}
// UpdateUser 更新一個(gè)用戶
func (s *UserService) UpdateUser(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error) {
return s.uc.UpdateUser(ctx, req)
}
// DeleteUser 刪除一個(gè)用戶
func (s *UserService) DeleteUser(ctx context.Context, req *v1.DeleteUserRequest) (*emptypb.Empty, error) {
_, err := s.uc.DeleteUser(ctx, req)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
注入到service.ProviderSet
package service
// ProviderSet is data providers.
var ProviderSet = wire.NewSet(
NewUserService,
...
)
將服務(wù)注冊(cè)到gRPC服務(wù)器當(dāng)中去:
package server
// NewGRPCServer new a gRPC server.
func NewGRPCServer(cfg *conf.Bootstrap, logger log.Logger,
userSvc *service.UserService,
) *grpc.Server {
srv := bootstrap.CreateGrpcServer(cfg, logging.Server(logger))
userV1.RegisterUserServiceServer(srv, userSvc)
return srv
}
這樣驰贷,我們就有了一個(gè)完整的用戶服務(wù)
盛嘿。