xorm简介
官方定义:
XORM is a Simple & Powerful ORM Framework for Go Programming Language
xorm是一个简单而强大的Go语言ORM库. 通过它可以使数据库操作非常简便。xorm的目标并不是让你完全不去学习SQL,我们认为SQL并不会为ORM所替代,但是ORM将可以解决绝大部分的简单SQL需求。xorm支持两种风格的混用。
本文是xorm的操作手册,后续有原创文章记录xorm实例代码。
申明:以下大部分内容,是本人在遍历性阅读官方文档时的摘抄,不是本人原创,目的是迁移到自己博客,方便后续学习与查阅。小部分内容为个人代码。
特性
-
支持 Struct 和数据库表之间的灵活映射,并支持自动同步
-
事务支持
-
同时支持原始SQL语句和 ORM 操作的混合执行
-
使用连写来简化调用
-
支持使用ID, In, Where, Limit, Join, Having, Table, SQL, Cols等函数和结构体等方式作为条件
-
支持级联加载 Struct
-
Schema支持(仅Postgres)
-
支持缓存
-
通过 xorm.io/reverse 支持根据数据库自动生成 xorm 结构体
-
支持记录版本(即乐观锁)
-
通过 xorm.io/builder 内置 SQL Builder 支持
-
上下文缓存支持
-
支持日志上下文
驱动支持
xorm 当前支持的驱动和数据库如下:
- Mysql5.* / Mysql8.* / Mariadb / Tidb
- Postgres / Cockroach
- SQLite
- MsSql
- Oracle
- github.com/mattn/go-oci8 (试验性支持)
安装
1 | go get xorm.io/xorm |
xorm使用
创建 Engine 引擎
单个ORM引擎,也称为Engine。一个 APP 可以同时存在多个 Engine 引擎,一个Engine一般只对应一个数据库。Engine 通过调用 xorm.NewEngine
生成,如:
1 | import ( |
日志
日志是一个接口,通过设置日志,可以显示SQL,警告以及错误等,默认的显示级别为 INFO。
engine.ShowSQL(true)
,则会在控制台打印出生成的SQL语句;engine.Logger().SetLevel(core.LOG_DEBUG)
,则会在控制台打印调试及以上的信息;
如果希望将信息不仅打印到控制台,而是保存为文件,那么可以通过类似如下的代码实现,NewSimpleLogger(w io.Writer)
接收一个io.Writer接口来将数据写入到对应的设施中。
1 | f, err := os.Create("sql.log") |
连接池
engine内部支持连接池接口和对应的函数。
- 如果需要设置连接池的空闲数大小,可以使用
engine.SetMaxIdleConns()
来实现。 - 如果需要设置最大打开连接数,则可以使用
engine.SetMaxOpenConns()
来实现。 - 如果需要设置连接的最大生存时间,则可以使用
engine.SetConnMaxLifetime()
来实现。
定义表结构体
xorm支持将一个struct映射为数据库中对应的一张表。
名称映射规则
跟名称相关的函数包含在 xorm.io/xorm/names
下。名称映射规则主要负责结构体名称到表名和结构体 field 到表字段的名称映射。由 names.Mapper
接口的实现者来管理,xorm 内置了三种 Mapper
实现:names.SnakeMapper
, names.SameMapper
和names.GonicMapper
。
- SnakeMapper 支持struct为驼峰式命名,表结构为下划线命名之间的转换,这个是默认的Maper;
- SameMapper 支持结构体名称和对应的表名称以及结构体field名称与对应的表字段名称相同的命名;
- GonicMapper 和SnakeMapper很类似,但是对于特定词支持更好,比如ID会翻译成id而不是i_d。
当前 SnakeMapper 为默认值,如果需要改变时,在 engine 创建完成后使用
1 | engine.SetMapper(names.GonicMapper{}) |
同时需要注意的是:
- 如果你使用了别的命名规则映射方案,也可以自己实现一个 Mapper。
- 表名称和字段名称的映射规则默认是相同的,当然也可以设置为不同,如:
1 | engine.SetTableMapper(names.SameMapper{}) |
Column 属性定义
我们在 field 对应的 Tag 中对 Column 的一些属性进行定义,定义的方法基本和我们写SQL定义表结构类似,以下代码为例,使用了sqlite3作为数据库,定义一个银行账户的表结构,与django orm类似,可以指定各字段的名称映射规则,还可以将字段设为唯一标识,添加乐观锁等等,这里不多赘述。
1 | // 银行账户 |
Go与字段类型对应表
如果不使用 tag 来定义 field 对应的数据库字段类型,那么系统会自动给出一个默认的字段类型,对应表如下:
go type’s kind | value method | xorm type |
---|---|---|
implemented Conversion | Conversion.ToDB / Conversion.FromDB | Text |
int, int8, int16, int32, uint, uint8, uint16, uint32 | Int | |
int64, uint64 | BigInt | |
float32 | Float | |
float64 | Double | |
complex64, complex128 | json.Marshal / json.UnMarshal | Varchar(64) |
[]uint8 | Blob | |
array, slice, map except []uint8 | json.Marshal / json.UnMarshal | Text |
bool | 1 or 0 | Bool |
string | Varchar(255) | |
time.Time | DateTime | |
cascade struct | primary key field value | BigInt |
struct | json.Marshal / json.UnMarshal | Text |
Others | Text |
表结构操作
xorm 提供了一些动态获取和修改表结构的方法,通过这些方法可以动态同步数据库结构,导出数据库结构,导入数据库结构。
获取数据库信息
- DBMetas()
xorm支持获取表结构信息,通过调用 engine.DBMetas()
可以获取到数据库中所有的表,字段,索引的信息。
- TableInfo()
根据传入的结构体指针及其对应的Tag,提取出模型对应的表结构信息。这里不是数据库当前的表结构信息,而是我们通过struct建模时希望数据库的表的结构信息
表操作
- CreateTables()
创建表使用 engine.CreateTables()
,参数为一个或多个空的对应Struct的指针。同时可用的方法有 Charset() 和 StoreEngine(),如果对应的数据库支持,这两个方法可以在创建表时指定表的字符编码和使用的引擎。Charset() 和 StoreEngine() 当前仅支持 Mysql 数据库。
- IsTableEmpty()
判断表是否为空,参数和 CreateTables 相同
- IsTableExist()
判断表是否存在
- DropTables()
删除表使用 engine.DropTables()
,参数为一个或多个空的对应Struct的指针或者表的名字。如果为string传入,则只删除对应的表,如果传入的为Struct,则删除表的同时还会删除对应的索引。
创建索引和唯一索引
- CreateIndexes
根据struct中的tag来创建索引
- CreateUniques
根据struct中的tag来创建唯一索引
插入数据
插入数据使用Insert方法,Insert方法的参数可以是一个或多个Struct的指针,一个或多个Struct的Slice的指针。
如果传入的是Slice并且当数据库支持批量插入时,Insert会使用批量插入的方式进行插入。
- 插入一条数据,此时可以用Insert或者InsertOne
1 | user := new(User) |
在插入单条数据成功后,如果该结构体有自增字段(设置为autoincr),则自增字段会被自动赋值为数据库中的id。这里需要注意的是,如果插入的结构体中,自增字段已经赋值,则该字段会被作为非自增字段插入。
1 | fmt.Println(user.Id) |
- 插入同一个表的多条数据,此时如果数据库支持批量插入,那么会进行批量插入,但是这样每条记录就无法被自动赋予id值。如果数据库不支持批量插入,那么就会一条一条插入。
1 | users := make([]User, 1) |
- 使用指针Slice插入多条记录,同上
1 | users := make([]*User, 1) |
- 插入多条记录并且不使用批量插入,此时实际生成多条插入语句,每条记录均会自动赋予Id值。
1 | users := make([]*User, 1) |
- 插入不同表的一条记录
1 | user := new(User) |
- 插入不同表的多条记录
1 | users := make([]User, 1) |
- 插入不同表的一条或多条记录
1 | user := new(User) |
这里需要注意以下几点:
- 这里虽然支持同时插入,但这些插入并没有事务关系。因此有可能在中间插入出错后,后面的插入将不会继续。此时前面的插入已经成功,如果需要回滚,请开启事务。
- 批量插入会自动生成
Insert into table values (),(),()
的语句,因此各个数据库对SQL语句有长度限制,因此这样的语句有一个最大的记录数,根据经验测算在150条左右。大于150条后,生成的sql语句将太长可能导致执行失败。因此在插入大量数据时,目前需要自行分割成每150条插入一次。
查询和统计数据
所有的查询条件不区分调用顺序,但必须在调用Get,Exist, Sum, Find,Count, Iterate, Rows这几个函数之前调用。同时需要注意的一点是,在调用的参数中,如果采用默认的SnakeMapper
所有的字符字段名均为映射后的数据库的字段名,而不是field的名字。
查询条件方法
查询和统计主要使用Get
, Find
, Count
, Rows
, Iterate
这几个方法,同时大部分函数在调用Update
, Delete
时也是可用的。在进行查询时可以使用多个方法来形成查询条件,条件函数如下:
- Alias(string)
给Table设定一个别名
1 | engine.Alias("o").Where("o.name = ?", name).Get(&order) |
- And(string, …interface{})
和Where函数中的条件基本相同,作为条件
1 | engine.Where(...).And(...).Get(&order) |
- Asc(…string)
指定字段名正序排序,可以组合
1 | engine.Asc("id").Find(&orders) |
- Desc(…string)
指定字段名逆序排序,可以组合
1 | engine.Asc("id").Desc("time").Find(&orders) |
- ID(interface{})
传入一个主键字段的值,作为查询条件,如
1 | var user User |
如果是复合主键,则可以
1 | engine.ID(core.PK{1, "name"}).Get(&user) |
传入的两个参数按照struct中pk标记字段出现的顺序赋值。
- Or(interface{}, …interface{})
和Where函数中的条件基本相同,作为条件
- OrderBy(string)
按照指定的顺序进行排序
- Select(string)
指定select语句的字段部分内容,例如:
1 | engine.Select("a.*, (select name from b limit 1) as name").Find(&beans) |
- SQL(string, …interface{})
执行指定的Sql语句,并把结果映射到结构体。有时,当选择内容或者条件比较复杂时,可以直接使用Sql,例如:
1 | engine.SQL("select * from table").Find(&beans) |
- Where(string, …interface{})
和SQL中Where语句中的条件基本相同,作为条件
1 | engine.Where("a = ? AND b = ?", 1, 2).Find(&beans) |
- In(string, …interface{})
某字段在一些值中,这里需要注意必须是[]interface{}才可以展开,由于Go语言的限制,[]int64等不可以直接展开,而是通过传递一个slice。第二个参数也可以是一个*builder.Builder 指针。示例代码如下:
1 | // select from table where column in (1,2,3) |
- Cols(…string)
只查询或更新某些指定的字段,默认是查询所有映射的字段或者根据Update的第一个参数来判断更新的字段。例如:
1 | engine.Cols("age", "name").Get(&usr) |
- AllCols()
查询或更新所有字段,一般与Update配合使用,因为默认Update只更新非0,非”“,非bool的字段。
1 | engine.AllCols().Id(1).Update(&user) |
- MustCols(…string)
某些字段必须更新,一般与Update配合使用。
- Omit(…string)
和cols相反,此函数指定排除某些指定的字段。注意:此方法和Cols方法不可同时使用。
1 | // 例1: |
- Distinct(…string)
按照参数中指定的字段归类结果。
1 | engine.Distinct("age", "department").Find(&users) |
注意:当开启了缓存时,此方法的调用将在当前查询中禁用缓存。因为缓存系统当前依赖Id,而此时无法获得Id
- Table(nameOrStructPtr interface{})
传入表名称或者结构体指针,如果传入的是结构体指针,则按照IMapper的规则提取出表名
- Limit(int, …int)
限制获取的数目,第一个参数为条数,第二个参数表示开始位置,如果不传则为0
- Top(int)
相当于Limit(int, 0)
- Join(string,interface{},string)
第一个参数为连接类型,当前支持INNER, LEFT OUTER, CROSS中的一个值, 第二个参数为string类型的表名,表对应的结构体指针或者为两个值的[]string,表示表名和别名, 第三个参数为连接条件
1 | 详细用法参见 [5.Join的使用](5.join.md) |
- GroupBy(string)
Groupby的参数字符串
- Having(string)
Having的参数字符串
Get方法
查询单条数据使用Get
方法,在调用Get方法时需要传入一个对应结构体的指针,同时结构体中的非空field自动成为查询的条件和前面的方法条件组合在一起查询。
如:
- 根据Id来获得单条数据:
1 | user := new(User) |
- 根据Where来获得单条数据:
1 | user := new(User) |
- 根据user结构体中已有的非空数据来获得单条数据:
1 | user := &User{Id:1} |
或者其它条件
1 | user := &User{Name:"xlw"} |
返回的结果为两个参数,一个has
为该条记录是否存在,第二个参数err
为是否有错误。不管err是否为nil,has都有可能为true或者false。
Exist系列方法
判断某个记录是否存在可以使用Exist
, 相比Get
,Exist
性能更好。
1 | has, err := testEngine.Exist(new(RecordExist)) |
与Get的区别
Get与Exist方法返回值都为bool和error,如果查询到实体存在,则Get方法会将查到的实体赋值给参数
1 | user := &User{Id:1} |
建议
如果你的需求是:判断某条记录是否存在,若存在,则返回这条记录。
建议直接使用Get方法。
如果仅仅判断某条记录是否存在,则使用Exist方法,Exist的执行效率要比Get更高。
查询的其他方法
Find方法
查询多条数据使用Find
方法,Find方法的第一个参数为slice
的指针或Map
指针,即为查询后返回的结果,第二个参数可选,为查询的条件struct的指针。
Join的使用
- Join(string,interface{},string)
第一个参数为连接类型,当前支持INNER, LEFT OUTER, CROSS中的一个值, 第二个参数为string类型的表名,表对应的结构体指针或者为两个值的[]string,表示表名和别名, 第三个参数为连接条件。
1 | type UserGroup struct { |
Iterate方法
Iterate方法提供逐条执行查询到的记录的方法,他所能使用的条件和Find方法完全相同。
1 | err := engine.Where("age > ? or name=?)", 30, "xlw").Iterate(new(Userinfo), func(i int, bean interface{})error{ |
Count方法
统计数据使用Count
方法,Count方法的参数为struct的指针并且成为查询条件。
1 | user := new(User) |
Rows方法
Rows方法和Iterate方法类似,提供逐条执行查询到的记录的方法,不过Rows更加灵活好用。
1 | user := new(User) |
Sum系列方法
求和数据可以使用Sum
, SumInt
, Sums
和 SumsInt
四个方法,Sums系列方法的参数为struct的指针并且成为查询条件。代码实例参考https://gobook.io/read/gitea.com/xorm/manual-zh-CN/chapter-05/9.sums.html
更新数据
更新数据使用Update
方法,Update方法的第一个参数为需要更新的内容,可以为一个结构体指针或者一个Map[string]interface{}类型。当传入的为结构体指针时,只有非空和0的field才会被作为更新的字段。当传入的为Map类型时,key为数据库Column的名字,value为要更新的内容。
Update
方法将返回两个参数,第一个为 更新的记录数,需要注意的是 SQLITE
数据库返回的是根据更新条件查询的记录数而不是真正受更新的记录数。
1 | user := new(User) |
这里需要注意,Update会自动从user结构体中提取非0和非nil得值作为需要更新的内容,因此,如果需要更新一个值为0,则此种方法将无法实现,因此有两种选择:
- 1.通过添加Cols函数指定需要更新结构体中的哪些值,未指定的将不更新,指定了的即使为0也会更新。
1 | affected, err := engine.Id(id).Cols("age").Update(&user) |
- 2.通过传入map[string]interface{}来进行更新,但这时需要额外指定更新到哪个表,因为通过map是无法自动检测更新哪个表的。
1 | affected, err := engine.Table(new(User)).Id(id).Update(map[string]interface{}{"age":0}) |
乐观锁Version
要使用乐观锁,需要使用version标记
1 | type User struct { |
在Insert时,version标记的字段将会被设置为1,在Update时,Update的内容必须包含version原来的值。
1 | var user User |
更新时间Updated
Updated可以让您在记录插入或每次记录更新时自动更新数据库中的标记字段为当前时间,需要在xorm标记中使用updated标记,如下所示进行标记,对应的字段可以为time.Time或者自定义的time.Time或者int,int64等int类型。
1 | type User struct { |
如果你希望临时不自动插入时间,则可以组合NoAutoTime()方法:
1 | engine.NoAutoTime().Insert(&user) |
删除数据
删除数据Delete
方法,参数为struct的指针并且成为查询条件。
1 | user := new(User) |
Delete
的返回值第一个参数为删除的记录数,第二个参数为错误。
注意:当删除时,如果user中包含有bool,float64或者float32类型,有可能会使删除失败。具体请查看 FAQ
软删除Deleted
Deleted可以让您不真正的删除数据,而是标记一个删除时间。使用此特性需要在xorm标记中使用deleted标记,如下所示进行标记,对应的字段必须为time.Time类型。
1 | type User struct { |
在Delete()时,deleted标记的字段将会被自动更新为当前时间而不是去删除该条记录,如下所示:
1 | var user User |
那么如果记录已经被标记为删除后,要真正的获得该条记录或者真正的删除该条记录,需要启用Unscoped,如下所示:
1 | var user User |
执行SQL查询
Query
也可以直接执行一个SQL查询,即Select命令。在Postgres中支持原始SQL语句中使用 ` 和 ? 符号。
1 | sql := "select * from userinfo" |
当调用 Query
时,第一个返回值 results
为 []map[string][]byte
的形式。
Query
的参数也允许传入 *builder.Buidler
对象
1 | // SELECT * FROM table |
QueryInterface
和 Query
类似,但是返回值为 []map[string]interface{}
QueryString
和 Query
类似,但是返回值为 []map[string]string
执行SQL命令
也可以直接执行一个SQL命令,即执行Insert, Update, Delete 等操作。此时不管数据库是何种类型,都可以使用 ` 和 ? 符号。
1 | sql = "update `userinfo` set username=? where id=?" |
简洁版英文手册
Overview ¶
Package xorm is a simple and powerful ORM for Go.
Installation ¶
Make sure you have installed Go 1.11+ and then:
1 | go get xorm.io/xorm |
Create Engine ¶
Firstly, we should new an engine for a database
1 | engine, err := xorm.NewEngine(driverName, dataSourceName) |
Method NewEngine’s parameters is the same as sql.Open. It depends drivers’ implementation. Generally, one engine for an application is enough. You can set it as package variable.
Raw Methods ¶
XORM also support raw SQL execution:
- query a SQL string, the returned results is []map[string][]byte
1 | results, err := engine.Query("select * from user") |
- execute a SQL string, the returned results
1 | affected, err := engine.Exec("update user set .... where ...") |
ORM Methods ¶
There are 8 major ORM methods and many helpful methods to use to operate database.
- Insert one or multiple records to database
1 | affected, err := engine.Insert(&struct) |
- Query one record or one variable from database
1 | has, err := engine.Get(&user) |
- Query multiple records from database
1 | var sliceOfStructs []Struct |
- Query multiple records and record by record handle, there two methods, one is Iterate, another is Rows
1 | err := engine.Iterate(...) |
- Update one or more records
1 | affected, err := engine.ID(...).Update(&user) |
- Delete one or more records, Delete MUST has condition
1 | affected, err := engine.Where(...).Delete(&user) |
- Count records
1 | counts, err := engine.Count(&user) |
- Sum records
1 | sumFloat64, err := engine.Sum(&user, "id") |
Conditions ¶
The above 8 methods could use with condition methods chainable. Attention: the above 8 methods should be the last chainable method.
- ID, In
1 | engine.ID(1).Get(&user) // for single primary key |
- Where, And, Or
1 | engine.Where().And().Or().Find() |
- OrderBy, Asc, Desc
1 | engine.Asc().Desc().Find() |
- Limit, Top
1 | engine.Limit().Find() |
- SQL, let you custom SQL
1 | var users []User |
- Cols, Omit, Distinct
1 | var users []*User |
- Join, GroupBy, Having
1 | engine.GroupBy("name").Having("name='xlw'").Find(&users) |
More usage, please visit http://xorm.io/docs
总结
XORM是基于Go语言的,功能全面且强大的ORM库。
本文是xorm的操作手册,后续有原创文章描述本人的xorm实例代码。
申明:以上大部分内容,是本人在遍历性阅读官方文档时的摘抄,不是本人原创,目的是迁移到自己博客,方便后续学习与查阅。小部分内容为个人代码。
资料来源: