从 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 进行了简单的封装。

在我的项目中,连接了两个数据库:

  1. data1 数据库包含 active
  2. 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__ 来指定这个表对应的数据库。

上面两个表的结构是一致的,它们的内容如下所示:

data2.register

对应的多数据库配置,默认的数据库指向 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.activedata2.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 的表定义

activeregister 表的定义如下:

 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}

上面列出的 ActiveModelRegisterModel 是具体的数据表定义,要将数据库中查询到的数据进行 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.activedata2.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(&registers).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 版本完全相同。

参考


阅读系列所有文章:从 Flask 到 Gin

全文完