Go 中 ORM 的 Repository(仓储)模式

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 (service *service) 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: service}
    return
}

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

这不大道至简。

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

原文地址:https://www.cnblogs.com/johnwii/p/11751283.html

时间: 2024-08-29 16:57:04

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

在MVC程序中,使用泛型仓储模式和工作单元实现增删查改

在这片文章中,我将自己动手为所有的实体:写一个泛型仓储类,还有一个工作单元. 工作单元的职责就是:为每一个实体,创建仓储实例.仓储(仓库)的职责:增删查改的功能实现. 我们将会在控制器中,创建工作单元类(UnitOfWork)的实例,然后根据实体,创建仓储实例,再就是使用仓储里面的方法,做操作了. 下面的图中,解释了,仓储和EF 数据上文的关系,在这个图里面,MVC控制器和仓储之间的交互,是通过工作单元来进行的,而不是直接和EF接触. 那么你可能就要问了,为什么要使用工作单元??? 工作单元,就

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

一:最普通的数据库操作 static void Main(string[] args) { using (SchoolDBEntities db = new SchoolDBEntities()) { db.Students.Add(new Student() { StudentName = "nihao" }); db.SaveChanges(); } } domain 和 db 是怎么操作... DbSet<Student> 集合 [用于存放集合] 从名称中可以看出,是

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

原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-operations-using-the-generic-repository-pattern-and-dep/ 系列目录: Relationship in Entity Framework Using Code First Approach With Fluent API[[使用EF Code-First方式和Fluent API来探讨EF中的关系]] Code First Mig

1.仓储模式在MVC应用程序中的使用

目录 1.仓储模式在MVC应用程序中的使用 2.泛型仓储模式在MVC应用程序中的使用 3.MVC Code-First和仓储模式的应用 4.待续.... 这篇文章中,我会解释仓储模式在MVC程序中的使用. 首先,我们需要理解什么是仓储模式[repository Pattern],来看看下面的图片 没有使用仓储模式的MVC应用程序:      使用了仓储模式的MVC应用程序: 仓储模式,是一个抽象层,它将数据库的访问逻辑,映射到实体的访问逻辑. 下面,我们来看做一个应用程序,来体验一下仓储模式吧.

仓储模式在MVC中的应用学习系列

好久没写博客了,学习的东西,还是需要记录下来,自己懂还得懂得表达出来,这才是最重要的.好了废话说多了,现在开始正题.     在这个系列中,我会把仓储模式和工作单元在MVC应用程序中的应用写出来.有不对的地方,欢迎大家指正. 目录 1.仓储模式在MVC应用程序中的使用 2.泛型仓储模式在MVC应用程序中的使用 3.MVC Code-First和仓储模式的应用 4.待续....

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

MVC+EF 理解和实现仓储模式和工作单元模式 原文:Understanding Repository and Unit of Work Pattern and Implementing Generic Repository in ASP.NET MVC using Entity Framework 文章介绍 在这篇文章中,我们试着来理解Repository(下文简称仓储)和Unit of Work(下文简称工作单元)模式.同时我们使用ASP.NET MVC和Entity Framework 搭

4.CRUD Operations Using the Repository Pattern in MVC【在MVC中使用仓储模式进行增删查改】

原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-using-the-repository-pattern-in-mvc/ 上一篇文章,讲到了MVC中基本的增删查改,这篇文章,我会继续说到,使用仓储模式,进行增删查改. 什么是仓储模式呢,我们先来了解一下:  仓储模式是为了在程序的数据访问层和业务逻辑层之间创建一个抽象层,它是一种数据访问模式,提供了一种更松散耦合的数据访问方法.我们把创建数据访问的逻辑代码写在单独的类中,或者类库中

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

原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-operations-using-the-generic-repository-pattern-and-uni/ 系列目录: Relationship in Entity Framework Using Code First Approach With Fluent API[[使用EF Code-First方式和Fluent API来探讨EF中的关系]] Code First Mig

仓储模式Repository的选择与设计

1.项目小,扩展性差 public interface IRepository<T> where T : class,new() { /// <summary> /// 创建对象 /// </summary> /// <param name="model"></param> /// <returns></returns> T Create(T model); /// <summary> //