技术解析

DAO 层和 ORM,能区分,但又不完全能区分,我裂开了
0
2021-06-02 20:57:50
idczone

desc:是这样的,小弟从 php 转到 go 一个月时间不够,项目的目录结构需要自己搭建。于是就找网上的项目组织目录参考,发现有一部分目录结构里有 dao 层的分层概念。这个在 java 里面比较常见,在 php 中用得比较多的是 orm,不知道 go 中是不是也适合这样搞。

于是引起了我一顿梳理 dao 和 orm 之间的关系。但仍有如下疑问:

  1. 究竟项目使用了 orm 之后,还需不需要再分一层 DAO 层出来?如果要分,此时的 DAO 层里面写的是什么逻辑?求 demo 举例
  2. ORM 不是正好替代了 DAO 的角色吗?还是说 DAO 可以通过 ORM 来实现?此时 DAO 层的实现是怎么样抗投诉服务器的?在 ORM 外面包一层,意义在哪里?
  3. 在项目代码分层上(语言无关),DAO 和 ORM 分别是什么层面上的东西?

真的纠结了三四天了,每天看完都有不同的结论,求大佬们指点一下你们在项目中是怎么划分的~


可以直接把 repository 注入到 Service 里面用,对于一些复杂的手工 SQL,包一个 Repository 再给 Service 用

有 demo 吗?

这里面说的 Repository 就是 DAO 层?

DAO,一般叫数据访问层。
ORM,说的是对象关系映射。
这二者的描述角度不同。
一个是从控制操作角度描述,一个是从模型设计角度描述。
DAO 指的是对数据库表操作的方法封装。
而 ORM 指的是实体(类)与数据库表之间的对应关系。
所以:
1 、有了 ORM,是否还需要 DAO ?不一定,取决于你的设计,以及各个语言的 ORM 框架功能不同而决定。
举例:比如我需要输出表 A 的行数,如果是单纯的 ORM,那我就只能直接获取本表的所有内容,返回一个 List,然后我在业务层再去获取 List的 size 。这明显是效率非常低下的。于是我就可以通过 DAO 层单独编写封装 SQL 去获取表 A 的行数,返回。
2 、DAO 和 ORM 冲突么?不冲突。

DAO 是代码分层的概念。
DAO 中使用 ORM 来进行数据访问,举个例子,假如某一天不想用 ORM 了,只需要替换掉 DAO 层中的实现即可。
题外话:其实如果用来写业务,GO 并不比其他语言拥有更好的开发效率。想不明白为什么这么多人转 GO

我觉得看项目复杂度,如果一个项目 2 个人写,可以一个人负责数据访问一个人负责业务对接,那就应该分一层 DAO 出来,如果是一个人用了 ORM 就没必要搞一个 DAO 出来,不然你改起来会贼蛋疼,个人经验所得 ,顺手推下以前写的 ORM https://github.com/bigpigeon/toyorm

你的意思是,DAO 层可以通过 ORM 来实现?那 dao 层实现的函数一般类似怎么样的呢?是类似于这样的函数吗?
```php
function queryByContentAndCreatedBy($content, $createdBy){
$sql = "SELECT * FROM content WHERE title like '%".$content."%' AND created_by=?";
$sqlQuery = new SqlQuery($sql);
$sqlQuery->setNumber($createdBy);
return $this->getList($sqlQuery);
}
```
还是只是封装了简单的 CURD ?就是之前没有用过(或者用过不知道),看看看具体的 dao 实现是什么样的,或者说什么样的函数适合写到 dao 中。

没错,是这个意思。DAO 层可以通过 ORM 实现增删改查,也可以不通过。
至于说需要封装什么方法,这就要看你的程序业务需求而定了。
最基础的当然是增删改查。
关键在于多一层封装,就把数据控制逻辑和业务逻辑解耦了。
以后更换数据控制方法,比如从 ORM 变成 SQL,或者从 SQL 变成 ORM,甚至是更换数据库类型等,都不会对上层业务造成影响。

dao 层通过 orm 来访问数据的话,那 dao 层的实现一般长什么样子呢?另外如果 dao 层的实现不够通用,那不是一种复杂的查询就要对应一个函数?这样 dao 里面每来一个复杂查询都新增一个函数吗?

这也就衍生出来说:DAO 层暴露出来的接口设计,一定要是业务驱动的,或者说是功能驱动的,而不能是技术驱动的。

dao 的实现一般是怎么样的?能否举个例子呢?说增删改查很模糊,没有具体感念

分一层 dao 出来,那 dao 里面具体些什么样的逻辑呢?能不能举个例子呢?

orm 就是封装了一些 crud, 原本需要你手动 set 参数的, 你传个对象进去, 就根据对象字段名帮你映射到 sql 语句的?上. 然后再帮你把返回的数据按列名映射到 java 类字段上.
dao 就是原本你的 crud 直接写在业务方法里, 现在有人说 no, 你这样写不 cool, 你要把 crud 全部写到一个单独的类里, 这样 cool.

这个层怎么分,可以多看看那几个架构模式。什么 DDD 、洋葱、六边形、清晰等等。
DAO 是比较旧时代的水平分层里的概念

用用户信息举例:
DAO 屏蔽了数据获取的逻辑,只提供一个 getUser(id) 的方法,不管里面怎么实现,需要数据用这个方法取就行。
然后看 DAO 内部,可以用很多方法去实现。
最简单的方法,直接查库,query('select * from users where id = ' + id),然后返回数据,需要什么数据就直接写 SQL
ORM 就是把常用的 SQL 封装好,这样用里面提供的类似 getUserById(id) 的方法就能省掉上面的字符串拼接、参数处理、校验、返回数据格式化等问题,直接返回映射好的 object 。
当然,不一定所有数据都存在关系数据库里,也可能存在 nosql 数据库、磁盘文件甚至需要通过网络请求其他服务,但不管存在哪,为了最大限度的减少这些乱七八糟的逻辑对业务逻辑的影响,只提供统一的操作数据的接口,这个就是 DAO 干的活。

那有了 orm 就不需要 dao 了吧?

当然是全都要,两者又不排斥。
ORM 是关系数据库表中每一行与面向对象语言每个实体类实例的映射。
DAO 是设计模式,把同类通表数据库操作都整理到一个类中。
实际上,ORM 的 Repository 就是 DAO 。
就是这么简单。

假如我有一个根据用户活跃度排序输出用户列表的需求,此时 orm 并没有直接的方法,我就需要在 dao 层搞一个类似 getUsersByActiveOrder(limit, offset)这样的方法?那每来一个复杂的查询就需要在 dao 层增加一个方法,不管我 dao 的方法里面写的是原始 sql 还是用 orm,是这个意思吗?

Repository 这个里面是怎么样的?有简单的例子举一下吗?

大概也就只用过 ActiveRecord 这种没有 Repository 的 ORM 的人,会对 ORM 与 DAO 有疑惑吧


https://docs.spring.io/spring-data/jpa/docs/1.3.0.RELEASE/reference/html/jpa.repositories.html
https://symfony.com/doc/current/doctrine.html#querying-for-objects-the-repository

dao 差不多就是包装了 curd 的一些方法吧,比如每次查询都会自动加上一些 id 、deletetime 等信息,然后接口更加的具体化,比如说 saveUser updateUser getUser 等

如果要说 Repository 就是 dao 的实现的话,那我们之前只是将 Repository 层的函数,放到了 model 层去,没有分这一层出来,这样 model 就 = model + dao 了,那为什么不将 dao 合并到 model 这一层呢?

这样看起来这些接口是业务驱动的,而不是用 orm 再封装一层

是的,正常来说,把所有数据操作封装在一起( DAO 类),好处远远大于开销,比如:
能方便的复用之前的逻辑,不用再复制粘贴一遍
可以快速查看在当前数据集上有哪些操作
调整下层数据结构或添加删除参数时不用去找每一个调用的地方
需要整个替换掉 ORM 时改动不会太大
测试时方便 mock 数据
等等等。
而开销仅仅是多几个 DAO 类。
哪怕只是为了让代码结构更清晰,这几个类都是有必要的。

简单的项目结构里 model 充当 DAO 的设计很合理,轻便且能满足大部分需求


用 repository 当然是为了通过依赖注入容器搞黑魔法了,比如运行 aop 面向切面编程。
如果都写到实体类 entity (也就是你说的 model )中

就相当于写死了,有 1 万个实体类,难道注入同样的东西一万遍?

如果不用实体类实例方法搞,而用是在实体类的中用静态方法就更惨,要修改静态方法的行为怕是得 jvm 底层修改了,而显然我们动不了 jvm 代码。

repository 通过依赖注入到容器,有没有例子呀?


这个你去看看 springboot 或者 symfony 的容器相关文档吧

不对啊,如果像你说的要用 repository 依赖注入到容器,那容器调用的时候不需要统一 repository 的接口方法吗?比如我 UserRepository 有 queryList 方法,ContentRepository 却没有 queryList 方法,那注入到容器的时候,不就会报错?


不同的类注入,当然是各自的类型了,不会有你说的情况。

dao 还是很有必要的,项目大了后 orm 对象满天飞 的话无法重构,单元测试也不好写,业务逻辑也不容易理清

symfony 的 容器注入,不是没有类型吗?


手动注入需要类型 class
自动注入 autowire 会根据参数类型自动确定

大部分的项目其实短平快方式效率更高。
设计模式、项目架构一类东西有时候过于工业化和笨重了。
特别是在国内,大部分互联网公司并不会太过于在意项目的可扩展性和维护性。为了跟进对手或者抢占市场都是以先实现功能为主,反正用的 java,代码写的再烂也能勉强调试维护下去。真的维护不动了就推翻重构或者重写。
如果项目竞争不过对手的话,这个项目可能还没到需要重写的地步可能就被关闭了。所以在国内的话,什么代码整洁、项目结构合理之类的东西并不能产生价值。
国外的话情况可能会不一样做项目就跟建房子一样,一步一步稳稳的来。像美帝那边的大厦都上百年了,国内的话可能三四十年就拆了重建吧。

生搬硬套不一定合适

ORM 基本上是单纯的从 db 里面拿数据
有些情况下需要对数据进行组装和变换,这些操作按规范是不放在 ORM 的,于是就多出一层 Dao
但是由于 java 的“过于规范化”(这点我觉得不完全是坏事,因场景而异),所以不需要 dao 处理的情况也需要先经过一层 dao,这是为了统一。

前段时间为了学习 Go 写了个开源项目,分了 service 层和 repository 层,repository 主要是用来操作某一个表的,例如根据 ID 查询可能会很多地方用到,repository 可以写一个 FindById 函数,参数是 ID,返回是数据库表的结构体,这样就不必每次都去拿全局 DB.Where("id = ?", id).First(&User{}) 查询了,也更方便检索有哪些地方用了这个函数。service 层主要也是写一个会复用到的函数,不复用的直接就用 repository 操作了。
项目地址是: https://github.com/dushixiang/next-terminal
代码写的比较水,楼主可以参考下。

orm 的定义基本就是 Model 里边对数据库的一个映射啦。dao 就是一个事务里边的的组合体就好啦。上边再来一个 service,基本就成型了。web 的话,还有一个 handler 的层。


https://xblog.lufficc.com/blog/the-core-conception-of-laravel
https://segmentfault.com/a/1190000004875930
看 UserRepository 是如何注入到 UserController ( 放 __construct 当参数,框架的 IoC 机制会将它自动注入) 里的。
这里 UserRepository 就是一个服务,UserController 就是消费这个服务的消费者。生产者消费者关系(谁是 service,谁在消费 service )搞清楚就 OK 了:有个生产者消费者的概念就 OK 了
关于 “分层目的是方便出错排错 / layered architecture 好在哪里” 的文章
https://learnku.com/articles/48397
https://learnku.com/articles/5813/overview-of-hierarchical-model-for-web-pyramid-development-framework
https://docs.microsoft.com/en-us/dotnet/architecture/modern-web-apps-azure/common-web-application-architectures

看语言表达能力的, 像 java 这种假泛型, 一般还是得针对每个表建立一个 dao 类. 真泛型像 c就不用这么麻烦

主要还是看 ORM 的表现能力,如果 ORM 的表现能力不足,使用 DAO 封一下可用让代码更为清晰。
如果 ORM 表现能力足够强,加 DAO 更多的是 Java 设计模式看的太多,有些生搬硬套。
注:
Python 和 Ruby 相关的应用就没看到有谁去加一层 DAO 。

orm 比较纯粹,一个 sturct 一般就是对应一张表
DAO 比较复杂,提供出来的函数中可能包含了多个 orm
纠结名词意义不大,自己写写就知道了

在写,就是觉得 ORM 用着挺方便的,加一层 DAO 是有好处,但是看着好像不太大的样子

DAO 里的代码看一看你就能发现它跟 ORM 的 Repository 部分的作用是一样的,封装了对模型的操作访问方法。
ORM 是对数据的映射关系,gorm 就是这个作用,gorm 一开始会写类型声明和映射关系,类似 Laravel eloquent 一开始声明和 hasmany()那些。
都可以对号入座的。

所以是有了 orm 之后其实不需要 dao 层了?

组合体怎么理解?

问题是不知道 dao 里面写的什么代码?!


我有个项目可以给你简单参考下我当初是怎么理解和怎么做的:
dto 看这里 https://github.com/54853315/weapp-MrsZhangPrivateKitchen-Backend/tree/master/pkg/api
引用这些 dto 的 orm 看这里 https://github.com/54853315/weapp-MrsZhangPrivateKitchen-Backend/tree/master/models

补充下上条:
由于我大部分 dto 比较“偷工减料”,dto 你看这一份: https://github.com/54853315/weapp-MrsZhangPrivateKitchen-Backend/blob/master/pkg/api/dto/user.go
然后再看下 user 控制器和 user model 是怎么与它交互的就差不多够了。

然后 dto 文件的生效,主要取决于 https://github.com/54853315/weapp-MrsZhangPrivateKitchen-Backend/blob/master/pkg/api/dto/init.go

看了一下,你的 model 层除了定义了实体类型以外,还在此层基于 orm 封装了一些数据存取函数,这样看你的 model 像是 DAO + model,那你的 DTO 的职责只是定义了数据传输的对象结构,不知道我这样理解有没有错

dao 可以跟 orm 共存啊,dao 作为在 mvc 设计模式下的数据访问层,这个数据到底是谁提供的,mysql?redis?mongo?hbase?
其实不关心啊,orm 作为一种特定数据存储组件的一个封装而已,对业务来说其实可以是黑盒;其实日常情况下,dao 可以封装缓存层,比如将 mysql 的数据拉到 redis 缓存,业务逻辑层无需关注是怎么缓存的,交给 dao 做就好了

所以 dao 层可以用 orm 去搞对吧?写的函数就类似于我 7 楼写的那样?

dao 是一种业务逻辑层,orm 是一种基础设施,它俩是两码事。这 ORM 还只是仅参与 DAO 层的基础设施,你要是碰到贯穿所有层次的基础设施,比如 Java 的 Spring,要是还想分层,估计直接死翘翘。

写法都是灵活的,就像 所说的那样,dao 方法定义是业务驱动,只关心我要什么,而在 dao 里,需要写 sql 的时候就写,不需要写比如 orm 封装了就不用写,看 orm 来定,也就是说 dao 里面跟 orm 是强耦合的,你需要对特定的 orm 做出适配,这些都是 dao 做的事. 综合来看如果你底层数据库组件有可能变化,或者不确定会不会变化,就可以 dao,orm 都共存.
在我看来,不管什么情况,共存看起来层次会分明点,即使是透传.

兄弟,你的理解不太对。
我在 Model 里做的事情就是 eloquent 里会做的事情。
我在 DTO 里做的事情就是 Repository 里会做的事情。

这又涉及到了 eloquent 和 Repository 的 区别了...
你在 model 里面定义了一些数据操作的函数,在 DTO 里面只有数据传输对象,我是这么理解的...
eloquent = 数据表映射 + 数据操作
如果你用了 Repository pattern,那数据操作应该放在 Repository,eloquent 仅仅是一些数据表映射,属性定义就好了

ORM 和 DAO 是不冲突的。Repository 和 DAO 不是一个概念 。
DAO 是单纯的对数据库进行增删改查的一些操作和特殊的业务逻辑。
一个好的 ORM 基本就代替了 DAO 层 90%的作用。可以看看 DotNet EntityFramework Core ORM 基本没有比他还好用的
如果你的项目涉及到多种数据源需要提供给 Service 这里可以引入 Repository 的概念
使用了 ORM 也没有必要在对 ORM 进行一层封装,这种封装我认为是脱裤子放屁。

最后一点我可能不太认同,不想 orm 在 service 层满屏飞,还是在 dao 封装一层好一点,即使是简单的调用

symfony 的 容器注入实际就是反射构造函数的参数,然后注入的。laravel 了是一样的

这样的封装 会导致失去 ORM 很多的特性,而且会导致 DAO 层越来越臃肿 出现很多 FindByXX 之类的 func

那想请教下,你认为 orm 和 dao 共存的时候,具体的使用场景是怎么样的?什么时候直接用 orm 进行链式调用,什么时候用 dao 呢?可不可以举个例子区分一下呢?十分感谢!

dao 层获取一个对象(或一堆数据),至于这个对象怎么获得的,用 MySQL 、MongoDB 、Redis 、ES 还是文本文件,调用 dao 的人并不关心。如果用 MySQL 就用 MySQL 的 orm (如 Java 的 hibernate,PHP 的 fluent ),如果用 Redis 就用 Redis 的 orm 。
以后你想重构为其他任何的数据层 orm,调用 dao 的人都不需要做任何改动。

在 Java 中,dao 一般是先定义接口类,然后写对应的实现类。实现类可以是任何 orm,然后通过 spring 注入你想要的 dao 实现类。无论之后别人想换任何奇奇怪怪的 orm 都没问题,只要照着接口类去实现,然后用 Junit 跑一遍测试,测试 OK 的话修改下 spring 的注入配置就行了。
dao 用接口的形式封装,对测试和重构都非常友好

明白,这是我期待的答案

考虑这个场景,查询用户信息,先从 Redis 缓存里面查,查不到再从数据库里面查
一般有如下文件:
user_dao.go
user_redis.go
user_db.go
你一直纠结的 ORM 只是在 db 这一层,dao 其实是更大的概念

这三个文件里面写的是怎样的代码呢?可以举一个更详细的例子吗?

数据地带为您的网站提供全球顶级IDC资源
在线咨询
专属客服