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.DB
为 db *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(仓储)模式的更多相关文章
- 从Entity Framework的实现方式来看DDD中的repository仓储模式运用
一:最普通的数据库操作 static void Main(string[] args) { using (SchoolDBEntities db = new SchoolDBEntities()) { ...
- DDD之:Repository仓储模式
在DDD设计中大家都会使用Repository pattern来获取domain model所需要的数据. 1.什么事Repository? "A Repository mediates b ...
- 6.在MVC中使用泛型仓储模式和依赖注入实现增删查改
原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-operations-using-the-generic-repository-pat ...
- 在MVC中使用泛型仓储模式和依赖注入实现增删查改
标签: 原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-operations-using-the-generic-repository ...
- MVC+EF 理解和实现仓储模式和工作单元模式
MVC+EF 理解和实现仓储模式和工作单元模式 原文:Understanding Repository and Unit of Work Pattern and Implementing Generi ...
- 4.在MVC中使用仓储模式进行增删查改
原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-using-the-repository-pattern-in-mvc/ 系列目录: ...
- 5.在MVC中使用泛型仓储模式和工作单元来进行增删查改
原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-operations-using-the-generic-repository-pat ...
- MVC5+EF6 入门完整教程十一:细说MVC中仓储模式的应用
摘要: 第一阶段1~10篇已经覆盖了MVC开发必要的基本知识. 第二阶段11-20篇将会侧重于专题的讲解,一篇文章解决一个实际问题. 根据园友的反馈, 本篇文章将会先对呼声最高的仓储模式进行讲解. 文 ...
- MVC5+EF6 入门完整教程11--细说MVC中仓储模式的应用
摘要: 第一阶段1~10篇已经覆盖了MVC开发必要的基本知识. 第二阶段11-20篇将会侧重于专题的讲解,一篇文章解决一个实际问题. 根据园友的反馈, 本篇文章将会先对呼声最高的仓储模式进行讲解. 文 ...
随机推荐
- 隐马尔可夫模型(HMM)及Viterbi算法
HMM简介 对于算法爱好者来说,隐马尔可夫模型的大名那是如雷贯耳.那么,这个模型到底长什么样?具体的原理又是什么呢?有什么具体的应用场景呢?本文将会解答这些疑惑. 本文将通过具体形象的例子来引入该模型 ...
- Nginx之负载均衡 :两台服务器均衡(填坑)
第一步,两台服务器都要安装好Nginx和Tomcat,我这边的安装的是Nginx 1.16.1 Tomcat9: 第二步,安装完成之后,选择你要做均衡的那台服务器,,打开其Nginx 配置文件,在se ...
- Java13 闪亮来袭,你是否还停留在 Java8
近期 Java 界好消息频传.先是 Java 13 发布,接着 Eclipse 也发布了新版本表示支持新版本的 Java 特性. 本文介绍了 Java 13 的新特性并展示了相关的示例. 2019 年 ...
- 基于appium的模拟单点或多点触屏操作
一.单点触控 TouchAction类:将一系列的动作放在一个链条中,然后将该链条传递给服务器,服务器接受该链条后,解析各个动作,逐个执行,TouchAction类提供了以下几种方法: 短按:pres ...
- 基于python的selenium常用操作方法(2)
9 多表单切换 在Web应用中经常会遇到frame/iframe表单嵌套页面的应用,WebDriver只能在一个页面上对元素识别与定位,对于frame/iframe表单内嵌页面上的元素无法直接定位.这 ...
- Linux - 几种方法来实现scp拷贝时无需输入密码
前言 在实际工作中,经常会将本地的一些文件传送到远程的机器上.scp是一个很好用的命令,缺点是需要手工输入密码. 如何在shell脚本中实现传输文件,而不用手工输入密码呢?接下来介绍三种方法. 一.建 ...
- 关于WIN7下IE8IE7浏览器无法安装微信支付商户证书的解决方案
关于WIN7下IE8IE7浏览器无法安装微信支付商户证书的解决方案 解决方案就是使用 chrome浏览器 默认的chorme浏览器 打开微信商户平台 会提示让安装控件 然后反复安装 其实要解决这个 ...
- sql server 查询出整数 (可灵活运用)
系统辅助用表,常用来获取数字,当然还有其他用途
- overflow-x:scroll失效问题解决
在移动设备上设置overflow-x:scroll,大部分机型都是展示正常的,在安卓哦5.0系统上,无论怎么样滚动条都不会生效,终于找到了解决办法: display: -webkit-box; // ...
- Kotlin介绍(非原创)
文章大纲 一.Kotlin简介二.Kotlin相比Java优势三.Kotlin与Java混合使用四.参考文章 一.Kotlin简介 1. 什么是Kotlin 安卓和Java,前者是最受欢迎的移动开 ...