一般的 web 项目大概会被分为这么几层:
dao/model
service/logic/repository
controller
view
大多数项目会做前后分离,所以后端作为 api 就剩下了三层。实际上这三层里除了 logic/service 之外都和具体的业务逻辑没什么关系。controller 负责参数绑定/校验,dao 负责和存储打交道。之前在 ast 一文中已经讲了怎么样减少 controller 中的工作。这里来说说怎么能够自动生成 dao 层代码。
可能平常只在固定的框架和 orm 下写代码的同学没有太关注过 db 层面的事情。先来讲讲 MySQL 里 information_schema 这个数据库的情况。援引官网的说明:
INFORMATION_SCHEMA provides access to database metadata, information about the MySQL server such as the name of a database or table, the data type of a column, or access privileges. Other terms that are sometimes used for this information are data dictionary and system catalog.
数据库的元数据:数据库名,表名,各表内的列详情,以及一些访问权限信息。
这里面对我们来说最有用的就是数据库名、表名和列的详情。日常开发 90% 的情况我们都是针对单表进行 CRUD,所以 dao 层大多数情况下也是为单表的 CRUD 来编写代码。
在 golang 的 web 系统里,一个比较靠谱的 dao 层的子模块可能长这样(这里用我们组正在重构使用的 sql builder 来举个例子):
package cardmapper
import (
"errors"
"time"
"upper.io/db.v3/lib/sqlbuilder"
)
const (
CardTableNamePrefix = "card"
CardDBName = "card_shop"
)
type Card struct{
ID int `db:"id"` //
Name string `db:"name"` //卡片名
PicURL string `db:"pic_url"` //图片链接
}
func CardGetOne(tableName string, fields []interface{}, where map[string]interface{}) (Card, error) {
var res Card
sess := db.Registry[CardDBName]
q := sess.Select(fields...).From(tableName)
if len(fields) == 1 {
if fieldStr, ok := fields[0].(string); ok && fieldStr == "*" {
q = sess.SelectFrom(tableName)
}
}
for key, val := range where {
q = q.And(key, val)
}
err := q.One(&res)
if err != nil {
return Card{}, err
}
return res, nil
}
func CardGetList(tableName string, fields []interface{}, where map[string]interface{}, orderBy []string) ([]Card, error) {
//略
}
func CardUpdate(tableName string, set map[string]interface{}, where map[string]interface{}) (int, error) {
//略
}
func CardCreate(tableName string, fieldMap map[string]interface{}) (int, error) {
//略
}
func CardCreateBatch(tableName string, fieldMapList []map[string]interface{}) (int, error) {
//略
}
func CardDelete(tableName string, where map[string]interface{}) (int, error) {
//略
}
为了不在查询完再断言来断言去,我们需要对每一张表都编写这样一份差不多的代码。可能有的程序员把这件事情当成了命运,就这么随便地接受了苦逼地重复搬砖,或者耍点小聪明,我从别的地方拷贝一份代码过来然后改一改。但遗憾的是,在大多数公司代码量都不会是你的 kpi。
我们来看看怎么用 information_schema 里的信息直接生成这部分代码。
mysql> desc columns;
+--------------------------+---------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------------------+---------------------+------+-----+---------+-------+
| TABLE_SCHEMA | varchar(64) | NO | | | |
| TABLE_NAME | varchar(64) | NO | | | |
| COLUMN_NAME | varchar(64) | NO | | | |
| DATA_TYPE | varchar(64) | NO | | | |
| COLUMN_TYPE | longtext | NO | | NULL | |
| COLUMN_COMMENT | varchar(1024) | NO | | | |
+--------------------------+---------------------+------+-----+---------+-------+
21 rows in set (0.01 sec)
上面的结果只挑出了我们需要的列,来看看内部都存了些什么东西:
mysql> select table_schema,table_name,column_name,data_type,column_type,column_comment from columns where table_schema='card_shop' and table_name = 'card';
+--------------+------------+-------------+-----------+---------------+----------------+
| table_schema | table_name | column_name | data_type | column_type | column_comment |
+--------------+------------+-------------+-----------+---------------+----------------+
| card_shop | card | id | int | int(11) | |
| card_shop | card | name | varchar | varchar(30) | 卡片名 |
| card_shop | card | pic_url | varchar | varchar(1024) | 图片链接 |
+--------------+------------+-------------+-----------+---------------+----------------+
3 rows in set (0.00 sec)
喔~这下我们代码里想要的要素都全了~
不过这里还存在一个问题,MySQL 里的数据类型和我们程序语言里的数据类型并不是一回事,在生成代码的时候还需要进行一次转换,下面是从 beego 里扒出来的一份类型转换表,仅供参考:
var typeMapping = map[string]string{
"int": "int", // int signed
"integer": "int",
"tinyint": "int8",
"smallint": "int16",
"mediumint": "int32",
"bigint": "int64",
"int unsigned": "uint", // int unsigned
"integer unsigned": "uint",
"tinyint unsigned": "uint8",
"smallint unsigned": "uint16",
"mediumint unsigned": "uint32",
"bigint unsigned": "uint64",
"bit": "uint64",
"bool": "bool", // boolean
"enum": "string", // enum
"set": "string", // set
"varchar": "string", // string & text
"char": "string",
"tinytext": "string",
"mediumtext": "string",
"text": "string",
"longtext": "string",
"blob": "string", // blob
"tinyblob": "string",
"mediumblob": "string",
"longblob": "string",
"date": "time.Time", // time
"datetime": "time.Time",
"timestamp": "time.Time",
"time": "time.Time",
"float": "float32", // float & decimal
"double": "float64",
"decimal": "float64",
"binary": "string", // binary
"varbinary": "string",
}
有了类型映射,万事俱备。
再来改一改上面的 dao 层的样例代码,让它变得更像一份代码模板:
package {{struct_name_lower}}mapper
import (
"errors"
"time"
"upper.io/db.v3/lib/sqlbuilder"
)
const (
{{struct_name}}TableNamePrefix = "{{table_name}}"
{{struct_name}}DBName = "{{database_name}}"
)
{{struct_def}}
func {{struct_name}}GetOne(tableName string, fields []interface{}, where map[string]interface{}) ({{struct_name}}, error) {
var res {{struct_name}}
sess := db.Registry[CardDBName]
q := sess.Select(fields...).From(tableName)
if len(fields) == 1 {
if fieldStr, ok := fields[0].(string); ok && fieldStr == "*" {
q = sess.SelectFrom(tableName)
}
}
for key, val := range where {
q = q.And(key, val)
}
err := q.One(&res)
if err != nil {
return {{struct_name}}{}, err
}
return res, nil
}
func {{struct_name}}GetList(tableName string, fields []interface{}, where map[string]interface{}, orderBy []string) ([]{{struct_name}}, error) {
//略
}
func {{struct_name}}Update(tableName string, set map[string]interface{}, where map[string]interface{}) (int, error) {
//略
}
func {{struct_name}}Create(tableName string, fieldMap map[string]interface{}) (int, error) {
//略
}
func {{struct_name}}CreateBatch(tableName string, fieldMapList []map[string]interface{}) (int, error) {
//略
}
func {{struct_name}}Delete(tableName string, where map[string]interface{}) (int, error) {
//略
}
剩下的不用教了吧!如果还是觉得迷惑,可以参考
https://github.com/cch123/api_code_gen/blob/master/model_generate.go
**关于争议:**本来觉得整洁规范的 dao 层和自动生成的 sql builder 代码封装是能够显著提升幸福感的事情,但是在线下和同事讨论的时候遇到了一些争议。
**争议①:**同事A 表示 sql builder 的方式欠妥,dao 层应该能够严格地控制对外提供的服务。我们应该手写 sql。否则 dba 认为这个系统完全不可控。
**争议②:**同事B 认为自动生成代码这件事情不够人性化,接手别人自动生成的代码会有抵触心理。
**争议③:**同事C 觉得你这是完全落后的贫血模型,我先表示一下对你这个垃圾设计的鄙视。其它的之后再说。
先说②,这件事情归根结底在于这份生成出来的代码大家看着爽不爽,如果生成的代码是给某个项目组来用,那大家最好能够在模板上达成一些共识。然后再基于共识的模板来生成代码。这样至少可以保证这一层的代码的规范性,问题不大。但是我的意见是这一部分一定要生成,不要手写。现在接手过的项目一旦经手的人多了,dao 层代码完全没法看。
然后是①,这件事情没有很好的解决办法。如果全手写的话,我曾经见过一个 dao 文件里写了几百行 SQL 的,很多时候你要写个新 query 都不知道原来有没有写过,代码膨胀了以后对 RD 来说很难维护。更有甚者为了偷懒,可能直接不看原来的代码写个新函数,和原来是一样的 SQL。和同事讨论了半天也没有达成共识,如果是你怎么看?
最后是③,呵呵呵。