Java中如何使用DAO_和service,ORM,repository,MVC的model是啥关系_-大宽宽
首先,不要被这些概念搞懵逼,而是要跳出来问自己的需求是什么?这些概念和工具都是用来解决需求的。此外,要明白某个概念和名词在诞生之后不断地演化和派生,以至于在不同上下问下会有略微不同的意思。
首先说下DAO。DAO的概念来自于单机数据业务系统。这样的系统以数据库为核心,上面搭配了一系列数据逻辑。而访问数据库最原始的方式是直接输入SQL。这样的系统虽然可以跑,但有几个问题:
1. 在Java的世界中一切都是对象,因此操作数据库的方式应该用对象而不是SQL。比方说User UserDAO.findByID(int userID),而不是SELECT * FROM User where user_id = ?。更进一步的,SQL的结果最好可以直接返回User这样的对象,而不是自己写一大堆字段映射,把SQL的ResultSet结果里的key, value; key, value……手工set到对象里。如果你用过JDBCTemplate,大概可以了解这个写法的愚蠢之处。
2. 不同数据库的语法实际上差异挺大,比如MySQL,PG,Oracle,SQLServer等对于类似的功能会有完全不同的写法,甚至关键字,引号和反引号的语义都不一样。最好有一套工具可以根据用户的逻辑代码来产生不同数据库的SQL,方便切换。你永远都不知道你家老板会不会钱烧光了买不起Oracle,或者因为新的合规需求要做信创。
3. 查询缓存。当你调用读取类SQL时,最好可以顺便缓存下,下次再读就不用去数据库查了。但同时,写入数据时要自动清理缓存。缓存通常会被ORM工具实现到session这个对象里,用本机内存实现。
基于这些诉求,诞生了DAO这个编程模式,以及相关工具。其能力就是提供一套Object-like API,屏蔽SQL差异,同时提供内建的缓存能力(一级缓存/二级缓存)。
留意DAO一开始是一个编程范式,也就是说你定义了个XXXDAO的class,定义了find,save,update,delete等方法。这些方法是不会魔法般自动就知道怎么访问数据库了。最土的办法就是要自己写,包括上面说的不同SQL方言的适配,缓存机制等。这样并没有让工作量变小,而仅仅是提供了一个抽象,让DAO上面的调用者不用理会这些细节。
但很快就出现一大把工具可以以各种形式让这个DAO的class可以比较快速甚至0成本的生成SQL。这些工具一般称为ORM工具,就是可以将Object和Relation部分做转换的工具。但实际上大多数ORM工具的能力远远超过O-R-M这个简单的映射。比较典型的DAO工具有python的SQLAlchemy,Ruby的ActiveRecord,nodejs的Sequence,go的GORM,Java的mybatis,jooq等。
比方说,在Go里定义一个User的数据结构,填充id,然后直接First一把。就能把User的数据查到并填充好。
type User struct {
// some fields
}
u := User{
ID: 123,
db.First(&u) // u 被查询并填充好。
而MyBatis需要用annotation或者xml来将某个DAO的操作翻译为SQL。一些工具可以让Java通过解析一些约定的method名字来产生SQL等。
再扩大一点,如果数据存储的不是数据库,而是某种KV,本地文件,OSS/S3这种对象存储,其他服务的rpc的结果,也可以做”DAO“。但这里一般就没有ORM工具帮忙了,需要自己想办法。比方说要访问Redis里的数据,读出来的数据要自己”反序列化“成Object。本地文件如果CSV,yaml或者json,也有比较多的工具转换成代码可访问的数据。
问题中提到了“一个DAO方法可以包含多个sql操作吗?”。答案是当然可以。因为DAO本来就是为了封装复杂的SQL操作诞生的。但大部分ORM工具是按照一个对象一张table/view的约定来设计,因此其原生能力没法自动做join这种操作。而有些ORM则有能力自动的去做Join,前提是用某种办法标记了几个类得“join”关联关系。比如用Hibernate去做数据建模,设计一个User有个Order,就可以用 @OneToMany来标记的User的orders字段,查询User时自动就把其order都查出来。
但需要留意的是,尽管一个DAO方法内部可以有多个sql语句,甚至是多个sql+kv+rpc+……的语句。但从设计上要体现【访问一个数据实体】。像”删除一个User就删除其Permission是否应该在DAO内部写”,这具体要看在业务设计上Permission是不是一个独立的实体,而是完全从属于User。如果是前者,一般是在业务层来对两个DAO操作,是“删除用户”这个业务逻辑去调配User的DAO和Permission的DAO完成功能。即使是从属,还有一个考虑点是删除Permission是否可以有特殊逻辑。比方说“白名单里的用户删除时不清理权限”,那总要查一下白名单,这个查询不应该放到DAO这个层级里。如果是Permission完全从属于User,又无业务逻辑,就用UserDAO的删除里直接写了删掉Permission是完全合理的。因此这完全是个设计问题,而不是名词概念的问题。
另外一个问题是事务。在SQL中事务很简单,就是简单的SQL指令。但到了ORM就不太容易表达。比如,原本语句是UserDAO.save(User user),但多了一个事务,这个代码就不能表达自己是不是在一个事务里。不同的ORM解决方式不同。python等就很直接:
with session.begin(): # 启动事务
session.add(some_object())
session.add(some_other_object())
而Java因为要让事务这个事情尽量透明,选择了用@Transaction + Threadlocal。数据访问代码内部实现会自己去检查在不在某个事务里。但不要以为这就是好事,因为当代码套代码时,代码结构无法总是能清晰的把事务表达好,反而可能引起不必要的事务和性能开销。
当程序复杂后,ORM的一些优点就会变成问题。比方说
- 在分布式系统中,ORM内建的缓存用处不大了,因为不同的节点各自做各自的缓存会严重影响数据一致性。因此一般业务上都会在DAO层之上自己再写一套缓存逻辑,再对外提供”统一数据抽象“。一般的互联网系统中都会禁用ORM的内建缓存。
- 代码里你可以随便写findByXXXX的函数,自动翻译成SQL。但高并发系统里查询用不带索引的方式做查询就是找死。因此开发者还是得关心最底层的SQL生成的对不对。如果用了JPA那种JQL,就必须得同时精通JQL和JQL产生的SQL才能搞明白最终的SQL是不是想要的。反过来,如果数据库报了个慢查询,也得花点精力才能能弄明白对应的是哪段代码。
- 再比如上面提到的关联,比如一个User关联了几十万Order,ORM一查系统就挂了。此时必须手工控制要不要查关联数据或者要查多少,如何排序如何分组,要不要上ES等。此时也许程序员会手工写代码分别查User和Order,并手工控制查询的逻辑(大厂的搬砖人都是干这些活……)
- 尽管ORM通常都提供数据的validator能力,利用ORM写入数据可以很好的控制数据合法性,但拦不住非法数据从数据库层面直接导入(比如做金融的企业会购买三方的金融数据,并定期导入自己的数据库)
- ……
因此尽管很多ORM工具有很多”高级功能“,但我个人比较喜欢ORM只提供基本能力,能把SQL的差异性屏蔽好,把O-R-Mapping做好,代码最好能一眼看出来产生的SQL大概是什么,就可以了。剩下的留给开发者,或者其他更好的工具来解决。
Repository是DDD里引申的概念。即他要提供的是一个抽象的”领域对象集“的数据库。你可以理解为他要干的事和DAO很类似,只不过会更高一层。这个”高“指的是抽象上的高一层。但DDD不在本文的讨论范围,就不展开了。如果你的需求仅仅是访问数据,没有抽象的必要,就没必要引入Repository。或者说某些代码框架/约定帮你生成了叫XXXRepository的代码,但实际上也只是当做DAO来用而已。
但为了说明问题我还是举个具体的例子。比方说想做一个用户数据管理系统,你有1张表user_info保存基本数据,比如姓名,生日,爱好,个人简述等。作为DAO可以先设计类,用ORM工具生成增删改查代码。你的代码里甚至都不需要出现DAO这个名字,完全用ORM提供的方法来完成这些功能。
但来了一个需求是要更换数据库,过渡期数据要双写。没有ORM可以自动支持同时双写(哪怕它支持多种数据库,一个session也只能写一个库)。此时就需要一个DAO层,封装get,find,save等方法,你需要自己处理双写逻辑。为两个数据库各自启动一个ORM的数据源对象,然后操作。比如:
class UserDAO {
@Autowired
private SomeORM mysqldb; // 连接到mysql的数据源
private SomeORM pgdb; // 连接到postgres的数据源
public void save(User user) {
mysqldb.save(user);
pgdb.save(user);
// 额外处理一个写成功,一个写失败的补偿逻辑等
public void getUser(int userID) User {
User user = mysqldb.User.get(userID); // 优先用mysql的,找不到用pg的
if user != null {
return user;
user = pgdb.User.get(userID);
// ...
你可以在代码上加更多东西来封装中这个数据访问层的功能。比方说
- 加个redis的cache
- 做异构降级,比如写入db失败时,先写redis,然后再写一个event到mq。另外补一个mq的消费来尝试将redis里新写入的数据补回到db
- 补充加解密功能。对于敏感的姓名,身份证号等数据,调用某个第三方的服务rpc,save时加密,get时解密。上游就可以不操心了。
- 写入数据库的同时写入ES,支持搜索能力
- 监控埋点,可以提供写入的qps,数据大小,错误率等指标。
- 屏蔽敏感字段。比如当前上下文的操作用户如果和这个用户不存在某些特定关系(好友,关注……),自动清空敏感的字段,只留下比如昵称和头像这样的字段。
这里并不限制实现方法。可以手工写,也可以配合annotation做动态注入。但关键点是,当业务上需要对User数据进行操作时,一定要调用UserDAO的方法。也只需要理解这个方法的使用即可,不需要考虑内部的细节。
然后有一天,有新需求,需要提供UserProfile数据。这个数据并不是针对一个数据表,而是很多个数据源。比如一个企业应用的UserProfile里除了基本的用户信息,还可能有
- 组织架构系统里的部门,办公区,职级
- 审批系统里的最近审批条目和结果
- 最近的日程和任务
这些信息来自于其他数据表读取,或者是其他业务系统的rpc。为了让这个数据使用起来很方便,就可以定义一个UserProfileRepository来实现将这些不同的数据汇总到一起的逻辑。
类似的,你也可以在这一层加cache,做降级等处理。
MVC中的Model的提法主要是用来说明如何去操控界面。他的要点是说,要区分View-Controller-Model这三部分代码。View只管渲染,model只管表达数据,而controller负责view/model的变化推动另一方变化的逻辑。三块代码不要混成一坨。当然细节上有很多处理手段,MVC只是个非常高层的代码风格的概念而已。就好像”晚饭吃川菜“,但具体吃啥还得看冰箱里剩的什么,超市能买啥以及老婆的心情。
回到MVC里的Model,他并不关心这个Model是怎么表达的。是直接读数据库也好,读文件也好,DAO也可以,Repository也可以。都是无所谓的。当你看到QQ邮箱里的邮件,就知道这个界面背后有一个表达邮件的”model“,但具体是什么不一定。
Manager是Java里比较扯淡的一面。因为Java设计里第一等公民里没有函数,因此本来很方便的可以用函数表达的东西,到了Java里就不得不创建个类叫XXXXManager。大概可以理解为一组工具代码,调用来实现功能即可。其他语言,如python,go等都没有这种限制,函数就是函数,不需要额外的概念,就没这破事。
最后总结一下,概念/模型/工具都是为需求服务的。要先区分自己的逻辑需要有哪些功能需求和技术需求,而这些需求怎么拆解分层最舒服。再去找能用得上的模型/概念/工具实现去落地实现。有时会发现不止一种设计合适,就能再细细的比较。对不同技术方案的收益/成本比较,才是技术设计里最该体现的内容了。
对于简单的需求,建议就是怎么方便怎么来,用最小的能work的代码来完成目标。需求复杂了再慢慢演进和重构,不用引入复杂的概念,也不必理会那些名词。
评论区
Denis Liu: 大宽宽是知乎最接地气的大佬 👍🏽2 💭广东 🕐2023-03-17 18:53:04
万里: JPA和repository很优美,相比之下DAO和Mybatis就有点死板了。复杂的查询现在越来越倾向于通过REST Endpoints来实现了。 👍🏽1 💭澳大利亚 🕐2023-03-15 19:43:40
没有知识的荒原: 关注了。 👍🏽0 💭四川 🕐2024-09-11 12:57:38
不爱写代码的里克: “Manager是Java里比较扯淡的一面。因为Java设计里第一等公民里没有函数,因此本来很方便的可以用函数表达的东西,到了Java里就不得不创建个类叫XXXXManager。”我们可以理解jdk8以后这个情况好一点了吗,出现了lambda 表达式和函数式接口。 👍🏽0 💭浙江 🕐2024-05-27 17:27:25
│ └── 大宽宽: 可以搜一下java的残废闭包[捂脸] 👍🏽0 💭北京 🕐2024-05-27 17:52:23
│ └── 不爱写代码的里克: 搜了,谢谢[发呆] 👍🏽0 💭浙江 🕐2024-05-27 20:28:03