从 Flask 到 Gin —— SQLAlchemy 和 gorm
文章目录
本文是 从 Flask 到 Gin 系列的第 4 篇。
本篇讲解在 Flask 和 Gin 中使用 MySQL 数据库的相关问题。在 Flask 中,我使用的是 Python 世界中最强大的 ORM 库: SQLAlchemy。在 Gin 的实现中,我选择了 gorm。
Golang 中的 ORM 也是百花齐放的状态,如果你不喜欢 gorm ,也可以参考 awsome-go 来选择。
SQLAlchemy 的表定义
我博客中写过几篇关于 SQLAlchemy 的文章。在 Flask 的项目中,我使用的是 FlaskSQLAlchemy 这个插件,它对 SQLAlchemy 进行了简单的封装。
在我的项目中,连接了两个数据库:
data1
数据库包含active
表data2
数据库包含register
表
下方的代码展示了这两个表在 SQLAlchemy 中的定义:
1# -*- coding: utf-8 -*-
2"""
3app.models.audible
4~~~~~~~~~~~~~~~~~~~
5
6data1.active
7data2.register
8"""
9
10# db 就是 flask_sqlalchemy.SQLAlchemy 的实例
11from mjp.app import db
12
13
14class LogActive(db.Model):
15 """ Log 中的活跃数据
16 """
17 __tablename__ = 'active'
18 __bind_key__ = 'data1'
19
20 # 就是 regional
21 gid = db.Column(db.INT, primary_key=True, index=True)
22
23 # 代表日期
24 date = db.Column(db.INT, primary_key=True, index=True)
25
26 # 渠道 ID
27 channel = db.Column(db.INT, primary_key=True, index=True)
28
29 # 数量
30 num = db.Column(db.INT, nullable=True)
31
32
33class LogRegister(db.Model):
34 """ Log 中的注册数据
35 """
36 __tablename__ = 'register'
37 __bind_key__ = 'data2'
38
39 # 就是 regional
40 gid = db.Column(db.INT, primary_key=True, index=True)
41
42 # 代表日期
43 date = db.Column(db.INT, primary_key=True, index=True)
44
45 # 渠道 ID
46 channel = db.Column(db.INT, primary_key=True, index=True)
47
48 # 数量
49 num = db.Column(db.INT, nullable=True)
在 SQLAlchemy 中,可以很灵活的使用参数语法来制定表字段的定义。也可以使用 __tablename__
来指定表名,使用 __bind_key__
来指定这个表对应的数据库。
上面两个表的结构是一致的,它们的内容如下所示:
对应的多数据库配置,默认的数据库指向 data1
。这意味着如果不在 class 定义中指定 __bind_key__
,则会认为这个表是位于 data1
:
1{
2 "SECRET_KEY": "YutjgVSPDERGyPayXrXbwsuF_SZWiVmUw3mD4YYD_kY=",
3 "SQLALCHEMY_DATABASE_URI": "mysql+pymysql://zrong:123456@127.0.0.1/data1",
4 "SQLALCHEMY_BINDS": {
5 "data1": "mysql+pymysql://zrong:123456@127.0.0.1/data1",
6 "data2": "mysql+pymysql://zrong:123456@127.0.0.1/data2"
7 }
8}
SQLAlchemy 中的多数据库查询
由于 data1.active
和 data2.register
这两个表的结构是一样的,我将其封装成同一个方法: _response_register_or_active
,通过传入不同的表定义来实现查询。下面代码中的 responseto
方法的定义已经在 从 Flask 到 Gin —— 处理 JSON 一文中介绍过了。
1def _parse_date(datestr):
2 if datestr is None:
3 return None
4 return int(dt.strftime('%Y%m%d'))
5
6
7def _response_register_or_active(DataTable):
8 """ 提供一个数据表作为参数,数据表的字段名称必须相同
9 返回一个响应对象
10 :param DataTable: 数据表 Class
11
12 :arg from_date: 起始日期
13 :arg to_date: 终止日期
14 """
15 # get_request_values 是一个获取查询的封装,等同于在 request.args 中查询,很容易实现,在此就不列出定义了
16 gids, from_date, to_date = get_request_values('gids', 'from_date', 'to_date', request_key='args')
17 from_date = _parse_date(from_date)
18 to_date = _parse_date(to_date)
19 if from_date is None or to_date is None or gids is None:
20 return responseto('请提供 from_date/to_date/gids!', code=401)
21 try:
22 gids = json.loads(gids)
23 if not isinstance(gids, list) or len(gids) == 0:
24 return responseto('gids 必须是一个列表!', code=401)
25 except Exception as e:
26 return responseto('gids 解析错误!', code=401)
27
28 gids = [item.r for item in rall]
29 results = DataTable.query.\
30 filter(DataTable.gid.in_(gids)).\
31 filter(and_(DataTable.date >= from_date, DataTable.date <= to_date)).\
32 with_entities(DataTable.gid, DataTable.date, func.sum(DataTable.num).label('num')).\
33 group_by(DataTable.gid, DataTable.date).\
34 all()
35 stat = [{
36 'gid': item.gid,
37 'num': int(item.num),
38 'date': item.date
39 } for item in results]
40 return responseto(stat=stat, gids=gids)
定义两个不同的路由,直接调用上面的 _response_register_or_active
就可以实现根据提供的日期分组返回 MySQL 中的数据:
1@audible.route('/register/', methods=['GET'])
2def register():
3 """ 获取注册数据
4 """
5 return _response_register_or_active(LogRegister)
6
7
8@audible.route('/active/', methods=['GET'])
9def active():
10 """ 获取活跃数据
11 """
12 return _response_register_or_active(LogActive)
在对 LogRegister 和 LogActive 这两个表进行定义的时候,我们已经通过 __bind_key__
指定了数据库,执行查询的时候,SQLAlchemy 会自行切换数据库进行查询。
让我们来测试一下:
1curl --request GET \
2 --url 'http://127.0.0.1:5001/audible/register/?from_date=20180801&to_date=20180802&gids=%5B1%2C53%5D'
结果为:
1{
2 "code": 200,
3 "error": false,
4 "gids": [
5 1,
6 53
7 ],
8 "stat": [
9 {
10 "date": 20180801,
11 "gid": 1,
12 "num": 819
13 },
14 {
15 "date": 20180802,
16 "gid": 1,
17 "num": 840
18 },
19 {
20 "date": 20180801,
21 "gid": 53,
22 "num": 680
23 },
24 {
25 "date": 20180802,
26 "gid": 53,
27 "num": 624
28 }
29 ]
30}
gorm 的表定义
active
和 register
表的定义如下:
1package models
2
3import (
4 "github.com/gin-gonic/gin"
5)
6
7// ActiveModel is a table for active users
8type ActiveModel struct {
9 Gid int `gorm:"PRIMARY_KEY,AUTO_INCREMENT"`
10 Date int `gorm:"PRIMARY_KEY,INDEX"`
11 Channel int `gorm:"PRIMARY_KEY,INDEX"`
12 Num int `gorm:"NOT NULL"`
13}
14
15// TableName for ActiveModel
16func (ActiveModel) TableName() string {
17 return "active"
18}
19
20// RegisterModel is a table for registered users
21type RegisterModel struct {
22 Gid int `gorm:"PRIMARY_KEY,AUTO_INCREMENT"`
23 Date int `gorm:"PRIMARY_KEY,INDEX"`
24 Channel int `gorm:"PRIMARY_KEY,INDEX"`
25 Num int `gorm:"NOT NULL"`
26}
27
28// TableName for RegisterModel
29func (RegisterModel) TableName() string {
30 return "register"
31}
需要注意的是,在 gorm 中需要使用 Struct Tags 来定义字段属性,使用 TableName 方法来定义表的名称。
文档 gorm models 定义 中解释得不够清晰,几条常用的我总结一下:
- 使用
column:列名
这样的语法来精确指定列名。 - 使用
type:SQL 类型
这样的语法来精确指定列字段类型。 - Struct Tags 中具体的定义之间可以使用分号或者逗号隔开。
- Struct Tags 中不区分大小写。
下面是一个混合的范例,很清晰,不解释了:
1// RegionalModel is a table for regional
2type RegionalModel struct {
3 R int `gorm:"type:smallint;PRIMARY_KEY,AUTO_INCREMENT"`
4 Name string `gorm:"type:varchar(100);NOT NULL,INDEX"`
5 Value string `gorm:"type:text;INDEX"`
6 Status int `gorm:"type:smallint;NOT NULL"`
7 CreateTime *time.Time `gorm:"type:smallint;column:createtime"`
8 UpdateTime *time.Time `gorm:"type:smallint;column:updatetime"`
9}
上面列出的 ActiveModel
和 RegisterModel
是具体的数据表定义,要将数据库中查询到的数据进行 JSON 序列化,还需要一些代码配合。请参考:从 Flask 到 Gin —— 处理 JSON -> 序列化。
gorm 中的多数据库初始化
在 database.go
中初始化多个数据库,以供使用:
1package models
2
3import (
4 "fmt"
5 "mjp/util"
6
7 "github.com/jinzhu/gorm"
8 _ "github.com/jinzhu/gorm/dialects/mysql" // mysql drive
9)
10
11// DefaultDB is a global object for MySQL call.
12var DefaultDB *gorm.DB
13
14// Data1DB is a global object for MySQL bind with name "data1"
15var Data1DB *gorm.DB
16
17// Data2DB is a global object for MySQL bind with name "data2"
18var Data2DB *gorm.DB
19
20func init() {
21 conf := util.ConfigInstance()
22 DefaultDB = initDB(conf.Databases.URI)
23 Data1DB = initDB(conf.DatabaseBind("data1"))
24 Data2DB = initDB(conf.DatabaseBind("data2"))
25 // defer DefaultDB.Close()
26}
27
28func initDB(uri string) *gorm.DB {
29 db, err := gorm.Open("mysql", uri)
30 if err != nil {
31 errmsg := fmt.Errorf("Connect %s error: %s", uri, err)
32 panic(errmsg)
33 }
34 db.DB().SetMaxIdleConns(10)
35 db.LogMode(true)
36 db.SingularTable(true)
37 return db
38}
要了解 util.ConfigInstance
的具体实现,请参考:Flask 到 Gin —— 读取配置文件。
gorm 中的多数据库查询
和 SQLAlchemy 类似,由于 data1.active
和 data2.register
这两个表的结构是一样的,我将其封装成同一个方法: registerOrActive
,通过传入不同的表定义来实现查询。下面代码中的 re2q.Responseto
方法的定义已经在 从 Flask 到 Gin —— 处理 JSON 一文中介绍过了。
1package routers
2
3import (
4 "fmt"
5 "mjp/models"
6 "mjp/re2q"
7 "mjp/util"
8 "strconv"
9 "strings"
10
11 "github.com/gin-gonic/gin"
12 "github.com/jinzhu/gorm"
13)
14
15// 返回一个处理过的 gorm 查询,以便进一步处理
16func registerOrActive(c *gin.Context, DataTable *gorm.DB) (defaultDBQry *gorm.DB) {
17 gids, gok := c.GetQuery("gids")
18 var gidsInt []int
19 if gok {
20 gidsString := strings.Split(gids, ",")
21 gidsInt = make([]int, len(gidsString))
22 for i, value := range gidsString {
23 gidInt, gidErr := strconv.Atoi(value)
24 if gidErr != nil {
25 re2q.Responseto(c, fmt.Sprintf("Convert gid %s got a error!", value), 401)
26 return
27 }
28 gidsInt[i] = gidInt
29 }
30 }
31
32 fromDate, fromDateErr := strconv.Atoi(c.Query("from_date"))
33 toDate, toDateErr := strconv.Atoi(c.Query("to_date"))
34 if fromDateErr != nil || toDateErr != nil {
35 re2q.Responseto(c, "Give from_date and to_date please!", 401)
36 return
37 }
38 // gids 必须存在
39 if gidsInt == nil {
40 re2q.Responseto(c, "gids please!", 401)
41 return
42 }
43
44 defaultDBQry = DataTable.Where("date BETWEEN ? AND ?", fromDate, toDate).Where("gid IN (?)", gidsInt)
45 return
46}
和 Flask 中类似,定义两个不同的路由,直接调用上面的 registerOrActive
就可以实现根据提供的日期分组返回 MySQL 中的数据:
1package routers
2
3import (
4 "fmt"
5 "mjp/models"
6 "mjp/re2q"
7 "mjp/util"
8 "strconv"
9 "strings"
10
11 "github.com/gin-gonic/gin"
12 "github.com/jinzhu/gorm"
13)
14
15// InitAudible register the audible api
16func InitAudible(router *gin.RouterGroup) {
17 router.GET("/register", AudibleRegister)
18 router.GET("/active", AudibleActive)
19}
20
21// AudibleActive return active users list
22func AudibleActive(c *gin.Context) {
23 // 使用 Data1 数据库
24 defaultDBQry := registerOrActive(c, models.Data1DB)
25 // 查找活跃数据
26 actives := []models.ActiveModel{}
27 findError := defaultDBQry.Find(&actives).Error
28 if findError != nil {
29 re2q.Responseto(c, findError.Error(), 503)
30 return
31 }
32 activesSerializer := models.ActivesSerializer{c, actives}
33 re2q.ResponsetoWithData(c, gin.H{
34 "stat": activesSerializer.Response(),
35 "code": 200,
36 })
37}
38
39// AudibleRegister return registred user list
40func AudibleRegister(c *gin.Context) {
41 // 使用 Data2 数据库
42 defaultDBQry := registerOrActive(c, models.Data2DB)
43 // 查找注册数据
44 registers := []models.RegisterModel{}
45 findError := defaultDBQry.Find(®isters).Error
46 if findError != nil {
47 re2q.Responseto(c, findError.Error(), 503)
48 return
49 }
50 registersSerializer := models.RegistersSerializer{c, registers}
51 re2q.ResponsetoWithData(c, gin.H{
52 "stat": registersSerializer.Response(),
53 "code": 200,
54 })
55}
这里的测试效果和 Python 版本完全相同。
参考
- Object-relational mapping
- The Python SQL Toolkit and Object Relational Mapper
- The fantastic ORM library for Golang
- Flask-SQLAlchemy
- 从 Flask 到 Gin —— 处理 JSON
阅读系列所有文章:从 Flask 到 Gin。
- 文章ID:2685
- 原文作者:zrong
- 原文链接:https://blog.zengrong.net/post/flask-to-gin-sqlalchemy-gorm/
- 版权声明:本作品采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可,非商业转载请注明出处(原文作者,原文链接),商业转载请联系作者获得授权。