用更轻的解决方案替换完整的ORM(JPA/Hibernate):推荐的加载/保存模式?

ele*_*ype 37 java sql repository-pattern mybatis jooq

我正在开发一个新的Java Web应用程序,我正在探索新的方法(对我来说是新的!)来保存数据.我主要有JPA和Hibernate的经验,但除了简单的情况,我认为这种完整的ORM会变得非常复杂.另外,我不喜欢和他们一起工作.我正在寻找一种新的解决方案,可能更接近SQL.

我正在调查的解决方案:

但与Hibernate相比,我有两个使用案例,我担心这些解决方案.我想知道这些用例的推荐模式是什么.


用例1 - 获取实体并访问其中一些相关的子孙实体.

  • 假设我有一个Person实体.
    • Person有一个相关的Address实体.
      • Address有一个相关的City实体.
        • City实体有一个name属性.

从人员实体开始,访问城市名称的完整路径是:

person.address.city.name
Run Code Online (Sandbox Code Playgroud)

现在,假设我PersonService使用以下方法从a加载Person实体:

public Person findPersonById(long id)
{
    // ...
}
Run Code Online (Sandbox Code Playgroud)

使用Hibernate,Person可以根据需要延迟加载与之关联的实体,因此可以访问person.address.city.name并确保我可以访问此属性(只要该链中的所有实体都不可为空).

但是使用我正在研究的3种解决方案中的任何一种,它都会更复杂.有了这些解决方案,有哪些推荐的模式来处理这个用例?在前面,我看到3种可能的模式:

  1. 可以通过所使用的SQL查询急切地加载所有必需的关联子孙实体.

    但是我在这个解决方案中看到的问题是,可能还有一些其他代码需要从实体访问其他实体/属性路径Person.例如,可能需要访问一些代码person.job.salary.currency.如果我想重用findPersonById()我已经拥有的方法,那么SQL查询将需要加载更多信息!不仅是相关address->city实体,还包括相关job->salary实体.

    现在如果还有10个其他地方需要从人员实体开始访问其他信息呢?我是否应该急切地加载所有可能需要的信息?或者可能有12种不同的服务方法来加载一个人实体?:

    findPersonById_simple(long id)
    
    findPersonById_withAdressCity(long id)
    
    findPersonById_withJob(long id)
    
    findPersonById_withAdressCityAndJob(long id)
    
    ...
    
    Run Code Online (Sandbox Code Playgroud)

    但是每当我使用一个Person实体时,我就必须知道装载它的是什么以及什么没有...它可能非常麻烦,对吧?

  2. 在实体的getAddress()getter方法中Person,是否可以检查地址是否已经加载,如果没有,则懒得加载它?这是现实生活中常用的模式吗?

  3. 是否有其他模式可用于确保我可以从加载的模型访问我需要的实体/属性?


使用案例2 - 保存实体并确保其相关和修改的实体也已保存.

我希望能够Person使用这个PersonService方法保存实体:

public void savePerson(Person person)
{
    // ...
}
Run Code Online (Sandbox Code Playgroud)

如果我有一个Person实体并且我改变person.address.city.name了其他的东西,我怎样才能确保在City保存时保留实体修改Person?使用Hibernate,可以很容易地将保存操作级联到关联的实体.我正在调查的解决方案怎么样?

  1. 我是否应该使用某种标志来了解在保存此人时还必须保存哪些相关实体?

  2. 是否有任何其他已知模式可用于处理此用例?


更新:有一个讨论有关在JOOQ论坛这个问题.

Luk*_*der 29

你的许多问题的答案很简单.你有三个选择.

  1. 使用您提到的三个以SQL为中心的工具之一(MyBatis,jOOQ,DbUtils).这意味着您应该停止考虑OO域模型和对象关系映射(即实体和延迟加载).SQL是关于关系数据的,而RBDMS非常擅长计算"急切获取"多个连接结果的执行计划.通常,甚至不需要过早的缓存,如果你确实需要缓存偶尔的数据元素,你仍然可以使用像EhCache这样的东西

  2. 不要使用任何以SQL为中心的工具并坚持使用Hibernate/JPA.因为即使你说你不喜欢Hibernate,你也在"思考Hibernate".Hibernate非常擅长将对象图持久化到数据库中.这些工具都不能像Hibernate一样被迫工作,因为他们的使命是别的.他们的任务是运行SQL.

  3. 采用完全不同的方式,选择不使用关系数据模型.其他数据模型(例如图表)可能更适合您.我把它作为第三种选择,因为你可能实际上没有那种选择,而且我对替代模型没有太多的个人经验.

请注意,您的问题并非专门针对jOOQ.尽管如此,使用jOOQ,您可以通过外部工具(如ModelMapper)将平面查询结果(从连接表源生成)的映射外部化到对象图.关于ModelMapper用户组的这种集成,有一个有趣的持续线索.

(免责声明:我为jOOQ背后的公司工作)

  • 我想你可能会错过我的答案的目的:-)我只是认为没有比使用Hibernate*"加载和持久化具有关联实体的实体"*更好的模式.因为能够做到这一点,你需要Hibernate提供的所有强大功能:缓存,身份处理,ORM等.如果你想以SQL方式解决问题,但*仍然*想*"保存实体及其相关联实体"*,然后你会再次实现Hibernate,在我看来.但也许,你不想听到这个;-)让我们看看在这个问题结束之前是否还有其他意见. (12认同)
  • 换句话说,jOOQ不会与Hibernate竞争.jOOQ面向那些不想构建Hibernate定制的架构的人,即以Java域模型为中心的架构. (4认同)
  • @electrotype:正如我所说.Hibernate很好地解决了这些模式.如果你不想使用Hibernate,你最终会在SQL级别上实现类似Hibernate的类似东西 - 即计算域中已更改数据的增量,然后以正确的顺序生成必要的CRUD语句.SQL做CRUD很糟糕.SQL擅长批处理操作,查询和数据分析.你从来没有声明*为什么*你不想使用Hibernate,因为对于*"保存实体及其相关实体"*,Hibernate确实是一个不错的选择! (3认同)
  • 我不认为*所有*应用程序都是这样的.实际上,我倾向于编写没有Java域模型的应用程序.我发现即使在XSLT生成的HTML代码中,也可以很容易地从关系数据模型中导航关系.换句话说,我总是在桌子上操作,而不是在实体上操作.人们已经习惯了JPA,但JPA只解决了"领域模型优先"的方法.像jOOQ这样的工具更适合"关系模型优先"方法.详情请访问:http://www.jooq.org/doc/3.1/manual/preface.当然,世界不是黑/白,所以你可以混合使用这两种工具 (3认同)

leo*_*loy 28

当不使用真正的ORM时,这种问题是典型的,并且没有银弹.一个简单的设计方法,对我来说,对于iBatis(myBatis)的(不是很大的)webapp来说,是使用两层来实现持久性:

  • 一个哑的低层:每个表都有它的Java类(POJO或DTO),其中的字段直接映射到表列.假设我们有一个PERSON表格,其中一个ADDRESS_ID字段指向一个ADRESS表格; 然后,我们有一个PersonDb类,只有一个addressId(整数)字段; 我们没有 personDb.getAdress()方法,只有平原personDb.getAdressId().这些Java类非常愚蠢(他们不了解持久性或相关类).相应的PersonDao类知道如何加载/持久化此对象.使用iBatis + iBator(或MyBatis + MYBatisGenerator)等工具可以轻松创建和维护该层.

  • 包含富域对象的更高级别的层:每个层通常是上述POJO 的图形.这些类还具有通过调用相应的DAO来加载/保存图形(可能是懒惰,可能带有一些脏标志)的智能.但重要的是,这些富域对象不会一对一地映射到POJO对象(或DB表),而是映射到域用例.确定每个图形的"大小"(它不会无限增长),并且从外部像特定类一样使用.所以,并不是你有一个丰富的Person类(有一些相关对象的不确定图),使用的是几个用例或服务方法; 相反,你有几个富人阶级,PersonWithAddreses, PersonWithAllData...每一个包裹某个十分有限的图形,有自己的持久性逻辑.这可能看起来效率低或笨拙,并且在某些情况下可能会这样,但是当您需要保存完整的对象图形时,用例实际上是有限的.

  • 另外,对于像表格报告这样的东西(特定的SELECTS返回一堆要显示的列),你不会使用上面的东西,而是直接和愚蠢的POJO(甚至可能是地图)

在这里查看我的相关答案


Gle*_*est 11

持久性方法

从简单/基本到复杂/丰富的解决方案范围是:

  • SQL/JDBC - 对象内的硬编码SQL
  • 基于SQL的框架(例如jOOQ,MyBatis) - 活动记录模式(单独的通用对象表示行数据并处理SQL)
  • ORM-Framework(例如Hibernate,EclipseLink,DataNucleus) - 数据映射器模式(每个实体的对象)加上工作单元模式(持久化上下文/实体管理器)

您试图实施前两个级别之一.这意味着将焦点从对象模型转移到SQL.但是您的问题要求将涉及对象模型的用例映射到SQL(即ORM行为).您希望从前三个级别中的一个级别添加第三级功能.

我们可以尝试在Active Record中实现此行为.但是这需要将丰富的元数据附加到每个Active Record实例 - 涉及的实际实体,它与其他实体的关系,延迟加载设置,级联更新设置.这将使其有效地成为隐藏的映射实体对象.此外,对于用例1和2,jOOQ和MyBatis不会这样做.

如何实现您的请求?

将简单的ORM行为直接实现到对象中,作为框架或原始SQL/JDBC上的小型自定义层.

用例1:存储每个实体对象关系的元数据:(i)关系是否应该是延迟加载(类级别)和(ii)是否发生了延迟加载(对象级别).然后在getter方法中,使用这些标志来确定是否进行延迟加载并实际执行此操作.

用例2:与用例1类似 - 自己动手.在每个实体中存储脏标志.针对每个实体对象关系,存储一个标志,描述是否应该级联保存.然后,当保存实体时,递归访问每个"保存级联"关系.写下发现的任何脏实体.

模式

优点

  • 调用SQL框架很简单.

缺点

  • 你的对象变得更加复杂.在开源产品中查看用例1和2的代码.这不是微不足道的
  • 缺乏对对象模型的支持.如果您在域中使用java中的对象模型,则它对数据操作的支持较少.
  • 范围蔓延和反模式的风险:上述缺失的功能是冰山一角.最终可能会在商业逻辑中重塑一轮和基础设施膨胀.
  • 非标准解决方案的教育和维护.JPA,JDBC和SQL是标准.其他框架或自定义解决方案则不然.

值得???

如果您具有相当简单的数据处理要求和具有较少实体数据的数据模型,则此解决方案很有效:

  • 如果是这样,太好了!做上面.
  • 如果没有,这个解决方案不合适并且代表了虚假的节省 - 即最终需要更长时间并且比使用ORM更复杂.在这种情况下,再看看JPA - 它可能比你想象的更简单,它支持CRUD的ORM加上复杂查询的原始SQL :-).


Ber*_*ium 8

在过去的十年里,我使用的是JDBC,EJB实体bean,Hibernate,GORM以及最终的JPA(按此顺序).对于我当前的项目,我已经回到使用普通JDBC,因为重点是性能.所以我想要

  • 完全控制SQL语句的生成:能够将语句传递给DB性能调优器,并将优化版本放回程序中
  • 完全控制发送到数据库的SQL语句的数量
  • 存储过程(触发器),存储函数(用于SQL查询中的复杂计算)
  • 能够无限制地使用所有可用的SQL功能(使用CTE的递归查询,窗口聚合函数,......)

数据模型在数据字典中定义; 使用模型驱动的方法,生成器创建辅助类,DDL脚本等.数据库上的大多数操作都是只读的.只有少数用例写.

问题1:抚养孩子

系统建立在一个用例上,我们有一个专用的 SQL语句来获取给定用例/请求的所有数据.一些SQL语句大于20kb,它们连接,使用以Java/Scala编写的存储函数计算,排序,分页等,结果直接映射到数据传输对象,后者又被送入视图(在应用程序层中没有进一步处理).因此,数据传输对象也是特定于用例的.它只包含给定用例的数据(仅此而已,仅此而已).

由于结果集已经"完全连接",因此不需要延迟/急切提取等.数据传输对象已完成.不需要缓存(数据库缓存很好); 例外:如果结果集很大(大约50 000行),则数据传输对象用作缓存值.

问题2:储蓄

在控制器从GUI合并回来之后,再次存在一个保存数据的特定对象:基本上是层次结构中具有状态(新的,删除的,修改的......)的行.这是一种手动迭代,可以将数据保存在层次结构中:使用生成SQL insertupdate命令的一些辅助类来保留新的或修改的数据.至于删除的项目,这被优化为级联删除(PostgreSql).如果要删除多行,也会将其优化为单个delete ... where id in ...语句.

同样这是用例特定的,因此它不涉及一般的approch.它需要更多代码行,但这些是包含优化的行.

到目前为止的经验

  • 不应低估学习Hibernate或JPA的工作量.应该考虑配置缓存所花费的时间,集群中的缓存失效,急切/延迟提取和调优.迁移到另一个Hibernate主要版本不仅仅是重新编译.
  • 不应该高估在没有ORM的情况下构建应用程序的工作量.
  • 直接使用SQL更简单 - 接近 SQL(如HQL,JPQL)是一样的,特别是如果你谈到你的数据库性能调优器
  • 运行冗长而复杂的查询时,SQL服务器速度极快,特别是与Scala编写的存储函数结合使用时
  • 即使使用特定于用例的SQL语句,代码大小也会更小:"完全连接"的结果集可以在应用程序层中节省大量的行.

相关信息:

更新:接口

如果Person逻辑数据模型中存在实体,则有一个类来持久化Person实体(用于CRUD操作),就像JPA实体一样.

但中心思想是,没有单一的Person从使用情况/查询/业务逻辑的角度来看实体:每个服务方法定义了它自己的概念Person,它仅包含正是由使用情况下所需的值.没有更多,没有更少.我们使用Scala,因此许多小类的定义和使用非常有效(不需要setter/getter样板代码).

例:

class GlobalPersonUtils {
  public X doSomeProcessingOnAPerson(
    Person person, PersonAddress personAddress, PersonJob personJob, 
    Set<Person> personFriends, ...)
}
Run Code Online (Sandbox Code Playgroud)

被替换为

class Person {
  List addresses = ...
  public X doSomeProcessingOnAPerson(...)
}

class Dto {
  List persons = ...
  public X doSomeProcessingOnAllPersons()
  public List getPersons()
}
Run Code Online (Sandbox Code Playgroud)

通过使用特定的情况下Persons,Adresses等:在这种情况下,Person已经汇集了所有相关数据.需要更多类,但不需要传递JPA实体.

请注意,此处理是只读的,结果由视图使用.示例:City从人员列表中获取不同的实例.

如果数据发生了变化,这是另一个用例:如果更改了某个城市,则会使用其他服务方法处理,并再次从数据库中提取该人员.