iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 23
0
DevOps

欸你這週GO了嘛系列 第 23

[DAY23]Golang也是有ORM-GORM

關於ORM的介紹可以看這邊 ORM 物件關聯對映,這邊就有說明了~~~
用raw sql語法操作db跟用orm其實沒有什麼誰好誰不好,如果sql語法寫起來跟吃飯喝水一樣easy的話,
不一定要用orm。
如果sql語法沒寫的那麼精準,也不想常常被DBA手刀打下去的話,是可以考慮使用orm來滿足工作需求。

Golang首推的ORM套件還是以 GORM為首選,
特點如下

  • 全功能ORM
  • 關聯(Has One,Has Many,Belongs To,Many To Many,多態,單表繼承)
  • Create,Save,Update,Delete,Find 中鉤子方法
  • 支持Preload、Joins的預加載
  • 事務,嵌套事務,Save Point,Rollback To Saved Point
  • Context,預編譯模式,DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用SQL 表達式、Context Valuer 進行CRUD
  • SQL 構建器,Upsert,數據庫鎖,Optimizer/Index/Comment Hint,命名參數,子查詢
  • 複合主鍵,索引,約束
  • 自動遷移
  • 自定義Logger
  • 靈活的可擴展插件API:Database Resolver(多數據庫,讀寫分離)、Prometheus…-
  • 每個特性都經過了測試的重重考驗
  • 開發者友好

以下與GORM V2版的安裝與實作流程

安裝gorm v2與gorm的mysql driver

$go get -u gorm.io/gorm
//v1不需要下面的mysql driver
$go get -u gorm.io/driver/mysql

實作

因為目前GORM為v2版,跟v1在初始化時有很大的不同

DB連線

V2版,mysql可參考 go-gorm/mysql的設定,跟v1版的差距也太大了~~

package main

import (
	"fmt"
	"time"
    //V2需要引用這package
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

const (
	UserName     string = "root"
	Password     string = "password"
	Addr         string = "127.0.0.1"
	Port         int    = 3306
	Database     string = "test"
	MaxLifetime  int    = 10
	MaxOpenConns int    = 10
	MaxIdleConns int    = 10
)

func main() { 
	//組合sql連線字串
	addr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True", UserName, Password, Addr, Port, Database)    
	//連接MySQL
	conn, err := gorm.Open(mysql.Open(addr), &gorm.Config{})
	if err != nil {
		fmt.Println("connection to mysql failed:", err)
		return
	}  
    //設定ConnMaxLifetime/MaxIdleConns/MaxOpenConns
	db, err1 := conn.DB()
	if err1 != nil {
		fmt.Println("get db failed:", err)
		return
	}
	db.SetConnMaxLifetime(time.Duration(MaxLifetime) * time.Second)
	db.SetMaxIdleConns(MaxIdleConns)
	db.SetMaxOpenConns(MaxOpenConns)	
}

model的定義

對gorm來說,model struct定義好,gorm才能進行mapping,mapping表請參考 models

type User struct {
    //gorm為model的tag標籤,v2版的auto_increment要放在type裡面,v1版是放獨立定義
	ID        int64     `gorm:"type:bigint(20) NOT NULL auto_increment;primary_key;" json:"id,omitempty"`
	Username  string    `gorm:"type:varchar(20) NOT NULL;" json:"username,omitempty"`
	Password  string    `gorm:"type:varchar(100) NOT NULL;" json:"password,omitempty"`
	Status    int32     `gorm:"type:int(5);" json:"status,omitempty"`
	CreatedAt time.Time `gorm:"type:timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP" json:"created_at,omitempty"`
	UpdatedAt time.Time `gorm:"type:timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP" json:"updated_at,omitempty"`
}

table 操作

V2與V1有個很明顯的不同點
原本的gorm.DB底下的HasTable(),AddIndex()等行為都移至gorm.DB).Migrator()底下
使用AutoMigrate來維持table一直都是最新的狀態,基本上如果model的struct有新增欄位才有影響,已存在或是刪除掉欄位不會自動drop column。
Migrator()支持CreateTable/DropTable/RenameTable/AddColumn/AlterColumn...等等會對table結構的操作,但是強烈建議不要這樣子做XD

package main

import (
	"fmt"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

const (
	UserName     string = "root"
	Password     string = "password"
	Addr         string = "127.0.0.1"
	Port         int    = 3306
	Database     string = "test"
	MaxLifetime  int    = 10
	MaxOpenConns int    = 10
	MaxIdleConns int    = 10
)

type User struct {
	ID        int64     `gorm:"type:bigint(20) NOT NULL auto_increment;primary_key;" json:"id,omitempty"`
	Username  string    `gorm:"type:varchar(20) NOT NULL;" json:"username,omitempty"`
	Password  string    `gorm:"type:varchar(100) NOT NULL;" json:"password,omitempty"`
	Status    int32     `gorm:"type:int(5);" json:"status,omitempty"`
	CreatedAt time.Time `gorm:"type:timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP" json:"created_at,omitempty"`
	UpdatedAt time.Time `gorm:"type:timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP" json:"updated_at,omitempty"`
}

func main() {

	//組合sql連線字串
	addr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True", UserName, Password, Addr, Port, Database)
	//連接MySQL
	conn, err := gorm.Open(mysql.Open(addr), &gorm.Config{})
	if err != nil {
		fmt.Println("connection to mysql failed:", err)
		return
	}

	db, err1 := conn.DB()
	if err1 != nil {
		fmt.Println("get db failed:", err)
		return
	}

	db.SetConnMaxLifetime(time.Duration(MaxLifetime) * time.Second)
	db.SetMaxIdleConns(MaxIdleConns)
	db.SetMaxOpenConns(MaxOpenConns)
    
    //產生table
	conn.Debug().AutoMigrate(&User{})
    //判斷有沒有table存在
    migrator := conn.Migrator()
    has := migrator.HasTable(&User{})
	//has := migrator.HasTable("GG")
	if !has {
		fmt.Println("table not exist")
	}
}

CREATE

create可透過model或是語法進行新增資料

    //新增model資料
    user := User{UserName: "tester", Password: "12333", Status: 1}
    //
	result := conn.Debug().Create(&user)
	if result.Error != nil {
		fmt.Println("Create failt")
	}
	if result.RowsAffected != 1 {
		fmt.Println("RowsAffected Number failt")
	}
    //執行結果:[rows:1] INSERT INTO `users` (`user_name`,`password`,`status`,`created_at`,`updated_at`) VALUES ('tester','12333',1,'2020-09-30 13:52:49.913','2020-09-30 13:52:49.913')
    
    //只insert特定欄位值
    conn.Debug().Select("UserName", "Password").Create(&user)
    //執行結果:INSERT INTO `users` (`user_name`,`password`) VALUES ('tester','12333')
    
    //不insert特定欄位值,但是這個語法好像遇到auto_increment的pk會Duplicate pk
    conn.Debug().Omit("status").Create(&user)
    //執行結果:INSERT INTO `users` (`user_name`,`password`,`created_at`,`updated_at`,`id`) VALUES ('tester','12333','2020-09-30 14:00:46.006','2020-09-30 14:00:46.006',10),ID自己產生於語法中
    
    //BATCH INSERT,v2版新增的特點
    	users := []User{{UserName: "tester", Password: "12333", Status: 1}, {UserName: "gger", Password: "132333", Status: 1}, {UserName: "ininder", Password: "12333", Status: 1}}

	result := conn.Debug().Create(&users)
	if result.Error != nil {
		fmt.Println("Create failt")
	}
	fmt.Println("result.RowsAffected:", result.RowsAffected)
    //執行結果:INSERT INTO `users` (`user_name`,`password`,`status`,`created_at`,`updated_at`) VALUES ('tester','12333',1,'2020-09-30 14:07:00.133','2020-09-30 14:07:00.133'),('gger','132333',1,'2020-09-30 14:07:00.133','2020-09-30 14:07:00.133'),('ininder','12333',1,'2020-09-30 14:07:00.133','2020-09-30 14:07:00.133')
    
    //除了用slice struct外,也可以用map進行新增資料,map跟struct有蠻大的差異,struct會有default值,map是沒指定的就不會出現在insert語法上
    conn.Debug().Model(&User{}).Create(map[string]interface{}{
		"UserName": "gg", "Password": "18",
	})
    //執行結果:INSERT INTO `users` (`password`,`user_name`) VALUES ('18','gg')
    
    //也可以執行raw語法
    conn.Exec("INSERT INTO `users` (`password`,`user_name`) VALUES (?,?)", "G123", "999")

SELECT

  • Find:取得全部筆數
  • First:根據pk升冪取得第一筆
  • Last:根據pk降冪取得第一筆
  • Take:取得一筆資料
    對gorm的內部定義來說,查無資料視為錯誤,所以要判斷查無資料還是是其他錯誤可以用下面語法判斷
    以下是v2版語法:
    errors.Is(result.Error, gorm.ErrRecordNotFound)
var user User
var users []User

res := conn.Debug().Find(&users)
fmt.Println(res.RowsAffected)
//SELECT * FROM `users`
conn.Debug().First(&user)
//SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1
conn.Debug().Take(&user)
//SELECT * FROM `users` WHERE `users`.`id` = 1 LIMIT 1
conn.Debug().Last(&user)
//SELECT * FROM `users` WHERE `users`.`id` = 1 ORDER BY `users`.`id` DESC LIMIT 1

WHERE Clause

where裡面的寫法類似raw sql語法,使用?做佔位符號

conn.Debug().Where("user_name = ?", "tester4").Find(&user)
//SELECT * FROM `users` WHERE user_name = 'tester4'
conn.Debug().Where("user_name IN ?", []string{"tester4", "tester3"}).Find(&users)
//SELECT * FROM `users` WHERE user_name IN ('tester4','tester3'),IN語法可以用slice
conn.Debug().Where("user_name LIKE ?", "%tester%").Find(&users)
//SELECT * FROM `users` WHERE user_name LIKE '%tester%'

有個很好用的語法Pluck,可以把select出來的值變成slice的

conn.Debug().Table("users").Pluck("user_name", &us)
//us: [tester1 tester2 tester3 tester4 tester tester tester tester tester tester tester gger ininder jinzhu gg gg 999]

還有更多的語法可以參考官方文件 GORM QUERY

UPDATE與DELETE

  • Save:更新整個struct
  • Update:更新單個欄位
  • Updates:更新多個欄位,使用strcut或是map
//使用Save
conn.First(&user)
user.Password = "GG"
conn.Debug().Save(&user)
//UPDATE `users` SET `user_name`='tester1',`password`='GG',`status`=1,`created_at`='2020-09-30 05:46:22',`updated_at`='2020-09-30 15:46:07.403' WHERE `id` = 1

//使用Update
conn.Debug().Model(&User{}).Where("id = ?", 1).Update("password", "helloGG")
//UPDATE `users` SET `password`='helloGG',`updated_at`='2020-09-30 15:48:45.676' WHERE id = 1

//使用Updates
conn.Debug().Model(&User{}).Where("id = ?", 1).Updates(User{UserName: "hello", Password: "GG"})
//UPDATE `users` SET `user_name`='hello',`password`='GG',`updated_at`='2020-09-30 15:51:27.78' WHERE id = 1
conn.Debug().Model(&User{}).Where("id = ?", 1).Updates(map[string]interface{}{"UserName": "hello", "Password": "GG"})
//UPDATE `users` SET `password`='GG',`user_name`='hello',`updated_at`='2020-09-30 15:51:27.784' WHERE id = 1

//DELETE
conn.Debug().Where("id = ?", "1").Delete(&User{})
//DELETE FROM `users` WHERE id = '1'

GORM V2版真的跟V1版差蠻多的,有興趣真的看官方文件,玩法多超多 XD


上一篇
[DAY22]Golang玩MySQL
下一篇
[DAY24]Golang也來玩NoSQL-Redis
系列文
欸你這週GO了嘛30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言