golang 操作mysql
mysql 和 mongodb 是两个 非常重要的数据库,一个是关系数据库,一个是非关系数据库。也是日常开发的必备了
今天介绍 go 如何连接 mysql,如何使用 orm 进行操作。
参考:
GORM 中文文档
gin 和 gorm 构建 RESTful API
支持的数据库:
应该就这些吧
安装
go get -u github.com/jinzhu/gorm
或者使用 mod 的方式 (推荐,方便)
module src
require (
github.com/jinzhu/gorm v1.9.15
)
快速开始
- 导入包:
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite" // 需要那个数据库的驱动,就导入那个,只需要初始化即可,所以前面加个 _
)
- 定义数据库表结构:
type Product struct {
gorm.Model // 继承 gorm 内置结构体
Code string
Price uint
}
- 连接数据库
db, err := gorm.Open("sqlite3", "test.db")
if err != nil {
panic("连接数据库失败")
}
defer db.Close()
Open 第一个参数是数据库类型,第二个参数是链接的数据库结构,比如:"root:520@/yxl?charset=utf8&parseTime=True&loc=Local"
有个疑问需要解决的:
Open 怎么链接这个数据库的
先酱一下,第一个参数:
Open 函数源码:
func Open(dialect string, args ...interface{}) (db *DB, err error) {
if len(args) == 0 {
err = errors.New("invalid database source")
return nil, err
}
.....
dbSQL, err = sql.Open(driver, source)
.....
然后再看一下 sql.Open(driver, source) 里面的 Open 源码 :
// sql.go 文件内
var (
driversMu sync.RWMutex
drivers = make(map[string]driver.Driver)
)
func Open(driverName, dataSourceName string) (*DB, error) {
driversMu.RLock()
driveri, ok := drivers[driverName] // map 类型里面的是接口类型
.....
看出来,第一个参数是 数据库的驱动的接口字符串,然后里面直接map 方式取出来的
第二个参数明天看吧
pass
- 自动迁移模式
db.AutoMigrate(&Product{})
- 新建数据
db.Create(&Product{Code: "L1212", Price: 1000})
- 读取 (单个数据)
var product Product
db.First(&product, 1) // 查询id为1的product
db.First(&product, "code = ?", "L1212") // 查询code为l1212的product
- 更新 - 更新product的price为2000
db.Model(&product).Update("Price", 2000) // product 是已经查到的数据了
- 删除 - 删除product
db.Delete(&product) // product 同样是已经查询到的一个
生成数据库的结构
下面具体介绍每个部分
- 支持 以下数据库的连接操作
import _ "github.com/jinzhu/gorm/dialects/mysql"
// import _ "github.com/jinzhu/gorm/dialects/postgres"
// import _ "github.com/jinzhu/gorm/dialects/sqlite"
// import _ "github.com/jinzhu/gorm/dialects/mssql"
主要讲解一下mysql 的连接操作
db, err := gorm.Open("mysql", "user:password@/dbname?
charset=utf8&parseTime=True&loc=Local")
defer db.Close()
Sqlite3 (轻量级数据库,用在手机等移动应用 )
db, err := gorm.Open("sqlite3", "/tmp/gorm.db")
defer db.Close()
- 迁移
迁移就是把对应的数据库模型更新到连接的数据库 中(包括创建 新的数据表和 添加表的字段 )
ps:自动迁移仅仅会创建表,缺少列和索引,并且不会改变现有列的类型或删除未使用的列以保护数据
db.AutoMigrate(&User{})
db.AutoMigrate(&User{}, &Product{}, &Order{})
// 创建表时添加表后缀
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})
- 检查表是否存在
// 检查模型`User`表是否存在
db.HasTable(&User{})
// 检查表`users`是否存在
db.HasTable("users")
两种方式也比较贴心
- 创建表
// 为模型`User`创建表
db.CreateTable(&User{})
我怎么感觉和迁移表没什么区别 ?
- 删除表
// 删除模型`User`的表
db.DropTable(&User{})
// 删除表`users`
db.DropTable("users")
// 删除模型`User`的表和表`products`
db.DropTableIfExists(&User{}, "products")
- 修改列
// 修改模型`User`的description列的数据类型为`text`
db.Model(&User{}).ModifyColumn("description", "text")
和修改字段的值差不多 :
db.Model(&todo).Update("title", c.PostForm("title")) // &todo 为已经查询到的数据
- 删除列
// 删除模型`User`的description列
db.Model(&User{}).DropColumn("description")
- 添加外键
// 添加主键
// 1st param : 外键字段
// 2nd param : 外键表(字段)
// 3rd param : ONDELETE
// 4th param : ONUPDATE
db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT")
这个看不太明白
- 索引
// 为`name`列添加索引`idx_user_name`
db.Model(&User{}).AddIndex("idx_user_name", "name")
// 为`name`, `age`列添加索引`idx_user_name_age`
db.Model(&User{}).AddIndex("idx_user_name_age", "name", "age")
// 添加唯一索引
db.Model(&User{}).AddUniqueIndex("idx_user_name", "name")
// 为多列添加唯一索引
db.Model(&User{}).AddUniqueIndex("idx_user_name_age", "name", "age")
// 删除索引
db.Model(&User{}).RemoveIndex("idx_user_name")
这个也看不太明白
关于模型的介绍
其实这个模型有很多东西要说的,但是说多了也记不住,挑几个好记的。
- gorm.Model
这个是gorm 内置的一个结构体,他长这样子:
type Model struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
你可以直接把他嵌入你的模型(当然随你,自己定义也可以啦
)
自定义字段
type User struct {
ID uint
CreatedAt time.Time
Name string
}
使用 gorm.Model
// 添加字段 `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`
type User struct {
gorm.Model
Name string
}
-
表名是结构体名称的复数形
type User struct {} // 默认表名是users
type todoModel{} // 默认表名是todo_models
-
自定义表名
1、给结构体添加 TableName() string 函数 string 即为表名
func (User) TableName() string {
return "profiles"
}
2、更具字段的值决定表名,可以复用结构体的继承:
func (u User) TableName() string {
if u.Role == "admin" {
return "admin_users"
} else {
return "users"
}
}
这样一个结构体就可以针对字段,来对应多个表了, 提高代码复用
3、全局禁用表名复数
db.SingularTable(true) // 如果设置为true,User
的默认表名为user
,使用TableName
设置的表名不受影响
4、更改默认表名
有时候你可能全局更改表名的规则
通过修改 gorm.DefaultTableNameHandler 函数为你自己定义的函数即可
gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string {
return "prefix_" + defaultTableName;
}
defaultTableName 应该就是最开始默认的表名了
表的列名是 字段名全小写 加下划线的形式
CreatedAt time.Time // 列名为 `created_at`
碰到 非首字母为大写的,会在他前面加一个下划线,并把大写字母转化为小写字母
5、当然和 一般序列化一样,我们也可以自定义字段名
使用 column 标记字段名称
// 重设列名
type Animal struct {
AnimalId int64 `gorm:"column:beast_id"` // 设置列名为`beast_id`
Birthday time.Time `gorm:"column:day_of_the_beast"` // 设置列名为`day_of_the_beast`
Age int64 `gorm:"column:age_of_the_beast"` // 设置列名为`age_of_the_beast`
}
6、 默认情况的主键
默认情况
ID uint // 字段ID
为默认主键
设置自己的主键:
AnimalId int64 `gorm:"primary_key"` // 设置AnimalId为主键
7、默认CreatedAt 为创建的时间
CreatedAt字段的记录将被设置为当前时间
db.Create(&user) // 将会设置`CreatedAt`为当前时间
// 要更改它的值, 你需要使用`Update`
db.Model(&user).Update("CreatedAt", time.Now())
8、字段 UpdatedAt 默认为更新的时间
db.Save(&user) // 将会设置`UpdatedAt`为当前时间
db.Model(&user).Update("name", "jinzhu") // 将会设置`UpdatedAt`为当前时间
这样看的话,创建的时候, UpdatedAt CreatedAt 的值是一样的
9、字段 DeletedAt 酱记录删除的时间
这里的删除是 软删除,即将字段DeletedAt设置为当前时间,查询的时候是查不到的
关于模型的外键,主键这里就不酱了。因为内容太多还是自己看看吧
CURD 这个是最重要的
创建数据
1、create
create 适合新增一条心数据
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
db.Create(&user)
2、save
其实save 和 create 都可以新增一条数据,区别是:
save 在数据存在的时候,是用来更新的,二create 只能保存一个心数据
查询
最重要的莫过于查询了
gorm 提供的查询很形象,都是关键的单词
1、First
直接取出符合条件的第一条数据
(当他前面有比如 where 的条件时,会在 first find 这种取数据的操作执行那些条件,根据逻辑是是这样的,因为 where 那些都没有表名,表名肯定是 传入的结构体中获取的)
First 默认是主键排序, 第一个
// 获取第一条记录,按主键排序
db.First(&user)
//// SELECT * FROM users ORDER BY id LIMIT 1;
First 第一个参数是要查询数据的结构体,如果有第二个参数,如果只是单独的值,那就是 主键 的值
2、Last
// 获取最后一条记录,按主键排序
db.Last(&user)
//// SELECT * FROM users ORDER BY id DESC LIMIT 1;
3、Find
db.Find(&users)
//// SELECT * FROM users;
4、Where
Where 用于写入过滤条件,后面可以和 first 或者 find 来搭配
where 的参数 第一个参数,如果是结构体或者 map 对象,或单个值(默认主键)那就是对应的查询条件。
如果不止一个参数,那第一个参数就是带占位符的字符串,第二、三... 个 参数是查询条件中 ? 的占位符的值
4.1 Where(Struct)
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
这里用 User 结构体就是为了方便,获取表名还是 后面的 First 吧(我猜的,这个要看源码才明白的)
4.2 Where(map)
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
4.3 Where(主键的Slice)
db.Where([]int64{20, 21, 22}).Find(&users)
4.4 Where 多个参数
in
db.Where("name in (?)", []string{"jinzhu", "jinzhu 2"}).Find(&users)
LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
and
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
Time
db.Where("updated_at > ?", lastWeek).Find(&users)
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
5 、 反条件 Not
Not 可以有一个参数或者几个参数,基本同where
一个参数可以是 结构体或者 map 或者 主键的值
// Struct
db.Not(User{Name: "jinzhu"}).First(&user)
//// SELECT * FROM users WHERE name <> "jinzhu";
db.Not([]int64{1,2,3}).First(&user)
两个参数,一个是语句,后面的占位值
db.Not("name = ?", "jinzhu").First(&user)
一个是字段名,后面的是字段值
// Not In
db.Not("name", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");
6、内联条件的查询
主键查询时,应仔细检查所传递的值是否为有效主键,以避免SQL注入
内联条件就是 把where 里面的条件放在了,first 或 finnd 的第一个参数后面了。
随便举两个例子吧:
// Map
db.Find(&users, map[string]interface{}{"age": 20})
db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
7、 Or条件查询
Or 可以和 where 搭配 (where 自己不能直接 or ? and 是有的)
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
//// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';
8、查询链
所有的条件可以链式调用
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Not("name = ?", "jinzhu").Find(&users)
9、FirstOrInit
基本同 first ,但是这个区分 找不到 用 给的 struct,map条件初始化 心数据
不是太理解,需要练习证实一下
10、Attrs
// Unfound
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user)
//// SELECT * FROM USERS WHERE name = 'non_existing';
//// user -> User{Name: "non_existing", Age: 20}
不太明白,用的时候在看吧
11、FirstOrCreate
获取第一个匹配的记录,或创建一个具有给定条件的新记录,用于struct, map条件
这个和 FirstOrInit 差不多
// Unfound
db.FirstOrCreate(&user, User{Name: "non_existing"})
//// INSERT INTO "users" (name) VALUES ("non_existing");
//// user -> User{Id: 112, Name: "non_existing"}
12、Select
选择返回的字段
db.Select("name, age").Find(&users)
//// SELECT name, age FROM users;
db.Select([]string{"name", "age"}).Find(&users)
//// SELECT name, age FROM users;
13、Order
指定排序字段
db.Order("age desc, name").Find(&users)
//// SELECT * FROM users ORDER BY age desc, name;
重排序设置为true以覆盖定义的条件
因为链式调用 前面的排序规则会直接作用于后面的条件
// ReOrder
db.Order("age desc").Find(&users1).Order("age", true).Find(&users2)
//// SELECT * FROM users ORDER BY age desc; (users1)
//// SELECT * FROM users ORDER BY age; (users2)
14 、 Limit
限制 返回个数
db.Limit(3).Find(&users)
//// SELECT * FROM users LIMIT 3;
链式查询使用 -1 取消限制个数
// Cancel limit condition with -1
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
//// SELECT * FROM users LIMIT 10; (users1)
//// SELECT * FROM users; (users2)
15 、Offset
用法同 Limit,只不过这个是跳过的数量。拿到后面的数据,和 Limit 是相反的
16、Count计数
db.Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Find(&users).Count(&count)
17、Group & Having
用到再说
18、 Join
用到再说,艹
指定表名
终于掠过了查询,艹内容太多了
就是说从哪个表创建数据,修改数据,都不以结构体名为区分了。直接指定表的名字。
// 使用User结构定义创建`deleted_users`表
db.Table("deleted_users").CreateTable(&User{})
var deleted_users []User
db.Table("deleted_users").Find(&deleted_users)
//// SELECT * FROM deleted_users;
很简单
更新
分为全更新还是部分更新
全更新:
1、Save: 把全部字段都重新:
db.First(&user)
user.Name = "jinzhu 2"
user.Age = 100
db.Save(&user)
更新更改字段
2、Update(单个属性), Updates(多个属性,map 或者 struct)
db.Model(&user).Update("name", "hello")
// 使用`map`更新多个属性,只会更新这些更改的字段
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
// 使用`struct`更新多个属性,只会更新这些更改的和非空白字段
db.Model(&user).Updates(User{Name: "hello", Age: 18})
// 对于下面的更新,什么都不会更新为"",0,false是其类型的空白值
db.Model(&user).Updates(User{Name: "", Age: 0, Actived: false})
3、选择(Select)或忽略(Omit)字段
Select
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
//// UPDATE users SET age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
以上更新默认会修改 UpdatedAt字段
原因是,会默认调用 BeforeUpdate, AfterUpdate Callbacks 方法。不想修改的话需要使用 UpdateColumn(类四 update) 单个属性
UpdateColumns (类似 updates)多个属性
删除/软删除
// 删除存在的记录
db.Delete(&email)
//// DELETE from emails where id=10;
db.Where("email LIKE ?", "%jinzhu%").Delete(Email{})
//// DELETE from emails where email LIKE "%jinhu%";
db.Delete(Email{}, "email LIKE ?", "%jinzhu%")
//// DELETE from emails where email LIKE "%jinhu%";
软删除
模型有DeletedAt字段,它将自动获得软删除功能
只将字段DeletedAt的值设置为当前时间
// 批量删除
db.Where("age = ?", 20).Delete(&User{})
//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
// 软删除的记录将在查询时被忽略
db.Where("age = 20").Find(&user)
//// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;
如果想绝对删除, 使用Unscoped:
// 使用Unscoped查找软删除的记录
db.Unscoped().Where("age = 20").Find(&users)
//// SELECT * FROM users WHERE age = 20;
// 使用Unscoped永久删除记录
db.Unscoped().Delete(&order)
//// DELETE FROM orders WHERE id=10;
使用 Unscoped 后,酱不会过滤掉deleted_at 字段了