ORM 在业务开发中一直扮演着亦正亦邪的角色。很多人赞颂 ORM,认为 ORM 与面向对象的契合度让代码简洁有道。但是不少人厌恶它,因为 ORM 隐藏了太多的细节,埋下了超多的隐患。在 Go 中,我们也或多或少接触过 ORM,但是,在查阅不少业务代码后发现,ORM 使用起来颇为滑稽,并且“雷隐隐雾蒙蒙”。

从 Entity Framework 谈起

Entity Framework 作为雄踞 Microsoft .NET Framework 以及 .NET Core 的杀手级 ORM 不论在使用上还是效率上都是数一数二的。并且 Entity Framework 自带 Repository 模式(仓储模式)可以说降低了开发者的使用门槛。举几个实际的例子:

WebAppContext entity = new WebAppContext();

[HttpGet]
public ActionResult Index(String verify, String email)
{
var databasemail = entity.Mails.Find(verify);
//code...
entity.Mails.Add(databasemail);
entity.SaveChanges();
//code...
}

可以看到,通过 Entity Framework 上下文,可以方便地检索到数据并在随后的使用中直接访问数据实体并按照直觉进行 CURD。

Go 里面的 ORM 是怎么做的呢?

Go 里面的 ORM 用法

下面的内容以 go-pg 为例。

Go 里对于 ORM 的用法就百花齐放了。一共见识过 4 种不同的用法:

Raw 查询式

Raw 查询实际上是很经典的使用方式,一般出报表、批量更新或者执行数据调整的脚本时非常有用,实际上新手刚刚接触到 Go,使用 ORM 也会倾向于使用 Raw 查询(简单)。所以滥用导致 Raw 查询实际上在代码中到处都是,几乎把 ORM 当作了数据库驱动在用。

func Query(sql string, params ...interface{}) ([]map[string]interface{}, error) {
rows, err := DB.Raw(sql, params...).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
list := []map[string]interface{}{}
for rows.Next() {
dest := make(map[string]interface{})
if scanErr := MapScan(rows, dest); scanErr != nil {
return nil, scanErr
}
list = append(list, dest)
}
return list, nil
}

这样做不是说不好,而是数据缺乏组织化,并且 []map[string]interface{} 这种东西在实际使用的时候很容易因为类型不具合翻车(panic)。所以 Raw 查询不是不好,而是滥用不好。一般使用 CTE、窗口函数之类的前置条件场景,使用 Raw 查询是合理的,但是需要注意对于 Raw 查询的复用:

func (service *DBService) cte(arg1, arg2 interface{}, domain ...interface{}) (sql string, args []interface{}) {
//code...
return
}

返回可以服用的 CTE 查询这样来降低雷同 Raw 查询出现的频次。

基础查询式

这种模式在 ORM 使用中相当常见。直接使用 ORM 传入模型然后执行检索,操作起来大约是这样的:

var entity = Entity{}
PostgreSQLConnection.Model(&entity).Where(`ID = $id`).Select()

看上去利用 ORM 的优势,就是查询出来的结果是一个结构化的实体,但实际上这样的模式实际上就是前面 Raw 查询模式的一个变种,不过相对更安全一些。这样的查询方式,利用 ORM 的模型映射,但是由于没有统一组织管理查询,使得整体看上去显得凌乱,也就是说,到处都是 PostgreSQLConnection.Model。并且,这样的模式与前面一样,无法在数据层面上完成逻辑表达。

数据层面的逻辑表达

例如,Corporation 实体实际上有 Staffs 的强关联数据,如果用这个模式,查询 Corporation.Staffs 应该去构建 Staff 模型,然后 WHERE 语句中添加 CORPORATION_ID 这样的参数信息。但是理论上我要查询到该企业的员工信息应该直接在该企业实体的 Staffs 属性或方法访问到才对。

当然,ORM 或提供改善这样的问题的能力。go-pg 提供一个关系数据引用检索的特性(但是这个特性 Issue 比较多...)来提供形如 .Staffs 的方法。不过需要在查询时显式声明检索,并且需要立即指定条件,最后拿到的 .Staffs 实际上是已经查出来的结果数据,灵活程度比较低(例如,只需要符合条件的 ID 列表)。

半仓储模式(或曰数据服务模式)

这个模式实际上是我之前用过的一种模式,这种模式将各类数据访问的逻辑封装起来成为一个数据服务:

type (
//IService 服务契约定义
IService interface {
Save(*models.Entity) error
Find(interface{}, ...func(*orm.Query)) (*models.Entity, error)
Where(models.Entity, ...func(*orm.Query)) ([]models.Entity, error)
Count(models.Entity, ...func(*orm.Query)) (int, error)
} service struct {
Pg *pg.DB
}
)

然后去实现对应的:

  • Save
  • Find
  • Where
  • Count

然后根据数据的逻辑关系添加其他的数据访问接口,例如 Corporation 的服务添加一个 Staffs 契约定义。

然后将这些服务集统一注册到服务对象:

type (
//Services 基础服务集合
Services struct {
Corporation corporation.IService
}
)

实际上这样的使用模式已经很接近终极形态了,虽然这样的模式已经构造了数据访问的统一入口,并且也尝试去解决数据层面的逻辑问题,但是这样的数据访问最大的问题是,换汤不换药:

service.Corporation.Staffs(corp, `ID IN (?)`, pg.In(array))

在上面的语句,看上去我通过 Corporation 的信息直接访问到了 Staffs,但是实际上对应的语义是:

用企业信息数据服务查询员工信息

而不是:

企业的员工信息

本质上没有解决前面两个的问题,大概就是农夫山泉和怡宝的区别。那么,像 Entity Framework 的仓储模式,Go 里怎么实现才能更加优雅呢?

仓储模式

我们不妨回到 Entity Framework 上下文声明:

namespace Tencent.Models
{
public class WebAppContext : DbContext
{
public WebAppContext() : base("name=WebAppContext") {}
public virtual DbSet<Entity> Entities { get; set; }
}
}

注意到了吗,Entity.Entities 实际上并不是 Entity 类型而是 DbSet<T> 类型。为什么前面三个方法没有本质区别就在于,它们全是使用了 Plain Ordinary Go Structure(POGS)来推演数据以及提供数据的访问。

要做到仓储模式,我们应该构建数据库上下文结构(Go Structure with Database Context):

type (
//Corporation 应用数据库模型
Corporation struct {
tableName struct{} `sql:"corporations"`
*models.Corporation db *pg.DB
}
) //Save 保存
func (c *Corporation) Save() (err error) {
if c.ID > 0 {
err = c.db.Update(c)
} else {
err = c.db.Insert(c)
}
return
} //Query 查询
func (c *Corporation) Query() (query *orm.Query) {
return c.db.Model(c)
}

也就是与数据库交互,并在实际业务中流动的实例应该随附关联的数据库上下文。这样的话,可以在 Corporation 的实例方法中去定义 Staffs 方法:

//Staffs 公司员工列表
func (c *Corporation) Staffs(valid ...bool) *orm.Query {
tables := c.db.Model((*User)(nil)).Where(`"corporation_id" = ?`, c.ID)
if len(valid) > 0 {
tables.Where(`"valid" IS ?`, valid[0])
}
q := c.db.Model().With("users", tables).Table("users")
return q
}

注意,这里返回的是一个 CTE 查询。相当于 .Staffs() 方法并没有去直接执行查询而是提供一个“该公司员工数据集”的前置查询条件。如果需要查询关联员工信息的 ID,实际上还需要:

var staffIDs []int

err := corporation.Staffs().Column("id").Select(&staffIDs)


的后继查询操作。

为了实现统一的仓储模式,可以将这些结构统一注册到一个 Repositories:

type (
//Service 数据库服务协议
Repository interface {
User(...*models.User) *User
Corporation(...*models.Corporation) *Corporation
} repository struct {
*pg.DB
}
) //NewService 在目标连接上新建服务
func NewRepository(db *pg.DB) Repository {
return &repository{db}
}

修改前面 Corporation 定义中的 db *pg.DBdb *repository,然后将 Corporation 的工厂方法注册到 Repository

//Corporation 企业数据库服务
func (repository *repository) Corporation(corp ...*models.Corporation) (entity *Corporation) {
if len(corp) == 0 {
corp = append(corp, nil)
} else if corp[0] != nil {
defer entity.Clean()
}
entity = &Corporation{Corporation: corp[0], db: repository}
return
}

至此,ORM with Repository in Go 就创建终了。Repository 模式有效隔离开了数据模型、数据库上下文模型,并且真的简化了 DB 访问的同时提供了数据层面的逻辑。如果业务中需要使用到 Go,还用到了 Go 的 ORM 来访问数据库,不妨借鉴 .NET 或 Java ORM 的做法。

这不大道至简。

本篇水文的前提是 ORM,都用 ORM 了谈什么大道至简。

Go 中 ORM 的 Repository(仓储)模式的更多相关文章

  1. 从Entity Framework的实现方式来看DDD中的repository仓储模式运用

    一:最普通的数据库操作 static void Main(string[] args) { using (SchoolDBEntities db = new SchoolDBEntities()) { ...

  2. DDD之:Repository仓储模式

    在DDD设计中大家都会使用Repository pattern来获取domain model所需要的数据. 1.什么事Repository? "A Repository mediates b ...

  3. 6.在MVC中使用泛型仓储模式和依赖注入实现增删查改

    原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-operations-using-the-generic-repository-pat ...

  4. 在MVC中使用泛型仓储模式和依赖注入实现增删查改

    标签: 原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-operations-using-the-generic-repository ...

  5. MVC+EF 理解和实现仓储模式和工作单元模式

    MVC+EF 理解和实现仓储模式和工作单元模式 原文:Understanding Repository and Unit of Work Pattern and Implementing Generi ...

  6. 4.在MVC中使用仓储模式进行增删查改

    原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-using-the-repository-pattern-in-mvc/ 系列目录: ...

  7. 5.在MVC中使用泛型仓储模式和工作单元来进行增删查改

    原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-operations-using-the-generic-repository-pat ...

  8. MVC5+EF6 入门完整教程十一:细说MVC中仓储模式的应用

    摘要: 第一阶段1~10篇已经覆盖了MVC开发必要的基本知识. 第二阶段11-20篇将会侧重于专题的讲解,一篇文章解决一个实际问题. 根据园友的反馈, 本篇文章将会先对呼声最高的仓储模式进行讲解. 文 ...

  9. MVC5+EF6 入门完整教程11--细说MVC中仓储模式的应用

    摘要: 第一阶段1~10篇已经覆盖了MVC开发必要的基本知识. 第二阶段11-20篇将会侧重于专题的讲解,一篇文章解决一个实际问题. 根据园友的反馈, 本篇文章将会先对呼声最高的仓储模式进行讲解. 文 ...

随机推荐

  1. Linux下科学计数法(e)转化为数字的方法 [shell中几种数字计算说明]

    科学计数法使用e标识数值,将科学计算学转化为数字的思路:按e右边的数字移动小数点位数.e右边的数字如果是负数,则向左移动小数点.示例如下: 1.2345678e2 = 123.45678 1.2345 ...

  2. PHP】获取客户端(浏览器)信息、获取客户端系统信息、获取服务器信息

    * 获取客户端浏览器信息 * @param null * @author https://blog.jjonline.cn/phptech/168.html * @return string */ f ...

  3. C#数组3(可变数组)

    using System; namespace class1 { class program { static void Main(string[] args) { ][];//这里的行必须定义好,但 ...

  4. C#关闭多线程程序

    Process[] processes = System.Diagnostics.Process.GetProcesses(); //获得所有进程 foreach (Process p in proc ...

  5. HTML5新标签与特性---多媒体

    多媒体标签 embed:标签定义嵌入的内容 audio:播放音频 video:播放视频 多媒体 embed(会使用) embed可以用来插入各种多媒体,格式可以是 Midi.Wav.AIFF.AU.M ...

  6. 1042. Flower Planting With No Adjacent

    题意: 本题题意为: 寻找一个花园的涂色方案,要求 1.花园和花园之间,不能有路径连接的,不能涂成相同颜色的 一共有4中颜色,花园和花园之间,至多有三条路径 我菜了 - - ,又没做出来.. 看答案 ...

  7. 什么是技术规划(TPP)?

    什么是技术? 1.技,巧也. ——<说文> 2.为了人类的目的而操纵自然世界的工具.机器.系统和技巧的集合. ——梅里特·罗·史密斯 3.人类都在利用自然和改造自然的过程中积累起来并在生产 ...

  8. 2、mongoDB的基本操作

    数据写入和查询: show dbs (查看有哪些数据库) use imooc (使用数据库) db.dropDatabase() 删除数据库 备注:在use的时候如果use一个不存在的表,在mongo ...

  9. 05justify-content

    display: flex; 的默认轴是x轴 justify-content: 设置主轴上的子元素排列的方式 所以在使用之前要确定好哪一个是主轴 /* justify-content:flex-sta ...

  10. 运行java程序

    使用方式: java类名 硬盘上有HelloWorld.class,那么类名就是HelloWorld java HelloWorld[运行先到class路径下] 一定要注意:java命令后面跟的不是文 ...