37

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

我目前正在研究的解决方案:

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


用例 1 - 获取一个实体并访问它的一些关联的子实体和孙实体。

  • 假设我有一个Person实体。
    • Person有一个关联的Address实体。
      • Address有一个关联的City实体。
        • 这个City实体有一个name属性。

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

person.address.city.name

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

public Person findPersonById(long id)
{
    // ...
}

使用 Hibernate,Person可以根据需要延迟加载person.address.city.name

但是使用我正在调查的 3 个解决方案中的任何一个,它都更加复杂。使用这些解决方案,处理此用例的推荐模式是什么?前期,我看到了 3 种可能的模式:

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

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

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

    findPersonById_simple(long id)
    
    findPersonById_withAdressCity(long id)
    
    findPersonById_withJob(long id)
    
    findPersonById_withAdressCityAndJob(long id)
    
    ...
    

    但是每次我使用一个Person实体时,我都必须知道它加载了什么,什么没有加载……这可能很麻烦,对吧?

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

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


用例 2 - 保存实体并确保其关联和修改的实体也被保存。

我希望能够Person使用此方法保存实体PersonService

public void savePerson(Person person)
{
    // ...
}

如果我有一个Person实体并且我更改person.address.city.name为其他内容,我如何确保在City保存时实体修改将保持不变Person?使用 Hibernate,可以很容易地将保存操作级联到关联的实体。我正在调查的解决方案呢?

  1. 我是否应该使用某种标志来知道当我保存此人时还必须保存哪些关联实体?

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


更新:在 JOOQ 论坛上有一个关于这个问题的讨论。

4

8 回答 8

30

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

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

  2. 不要使用任何那些以 SQL 为中心的工具并坚持使用 Hibernate / JPA。因为即使你说你不喜欢 Hibernate,你也是在“考虑 Hibernate”。Hibernate 非常擅长将对象图持久化到数据库中。这些工具都不能被迫像 Hibernate 那样工作,因为它们的任务是另外的。他们的任务是在 SQL 上进行操作。

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

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

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

于 2013-07-25T18:27:49.527 回答
30

这种问题在不使用真正的 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类(带有一些不确定的相关对象图),而是使用了几个用例或服务方法;相反,您有几个丰富的类PersonWithAddresesPersonWithAllData...每个都包装了一个特定的有限图,具有自己的持久性逻辑。这可能看起来效率低下或笨拙,在某些情况下可能是这样,但经常发生的是,当您需要保存完整的对象图时的用例实际上是有限的。

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

在这里查看我的相关答案

于 2013-07-29T15:00:27.843 回答
11

持久性方法

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

  • SQL/JDBC - 对象内的硬编码 SQL
  • SQL-Based Framework (eg jOOQ, MyBatis) - Active Record Pattern (单独的通用对象代表行数据并处理 SQL)
  • ORM 框架(例如 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 中为您的域使用对象模型,它将对数据操作的支持较少。
  • 范围蔓延和反模式的风险:上述缺失的功能只是冰山一角。最终可能会在业务逻辑中进行一些 Reinvent the Wheel 和 Infrastructure Bloat。
  • 非标准解决方案的教育和维护。JPA、JDBC 和 SQL 是标准。其他框架或自定义解决方案不是。

值得???

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

  • 如果是这样,太好了!做上面。
  • 如果不是,则此解决方案不合适,并且代表了错误的努力节省 - 即最终将花费更长的时间并且比使用 ORM 更复杂。在这种情况下,再看看 JPA——它可能比你想象的更简单,它支持用于 CRUD 的 ORM 以及用于复杂查询的原始 SQL :-)。
于 2013-07-29T06:47:50.560 回答
8

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

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

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

问题一:接孩子

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

由于结果集已经“完全加入”,因此不需要延迟/急切获取等。数据传输对象是完整的。不需要缓存(数据库缓存很好);例外:如果结果集很大(大约 50 000 行),则将数据传输对象用作缓存值。

问题2:保存

在控制器合并来自 GUI 的更改后,再次有一个特定的对象来保存数据:基本上是层次结构中具有状态(新、删除、修改......)的行。这是将数据保存到层次结构中的手动迭代:使用一些具有生成 SQLinsertupdate命令的帮助程序类来持久保存新的或修改的数据。至于删除的项目,这被优化为级联删除(PostgreSql)。如果要删除多行,则也将其优化为单个delete ... where id in ...语句。

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

迄今为止的经历

  • 不应低估学习 Hibernate 或 JPA 所付出的努力。应该考虑配置缓存、集群中的缓存失效、急切/延迟获取以及调优所花费的时间。迁移到另一个 Hibernate 主要版本不仅仅是重新编译。
  • 不应高估在没有 ORM 的情况下构建应用程序所付出的努力。
  • 直接使用 SQL 更简单 -接近SQL(如 HQL、JPQL)是一样的,尤其是当您与数据库性能调谐器交谈时
  • SQL 服务器在运行长而复杂的查询时速度非常快,尤其是与用 Scala 编写的存储函数结合使用时
  • 即使使用特定于用例的 SQL 语句,代码量也更小:“完全连接”的结果集在应用程序层中节省了很多行。

相关信息:

更新:接口

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

但是从用例/查询/业务逻辑的角度来看,没有单一Person的实体:每个服务方法都定义了自己的 a 概念Person,并且它只包含该用例所需的值不多也不少。我们使用 Scala,因此许多小类的定义和使用非常高效(不需要 setter/getter 样板代码)。

例子:

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

被替换为

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

class Dto {
  List persons = ...
  public X doSomeProcessingOnAllPersons()
  public List getPersons()
}

使用特定用例PersonsAdresses:在这种情况下,Person已经聚合了所有相关数据。需要更多类,但不需要传递 JPA 实体。

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

如果数据发生了变化,这是另一个用例:如果一个人的城市发生了变化,这将由不同的服务方法处理,并再次从数据库中获取该人。

于 2013-08-01T23:06:03.510 回答
2

警告:这是项目作者的另一个无耻插件。

查看JSimpleDB,看看它是否符合您的简单性与功能性标准。这是一个新项目,源于 10 多年试图通过 SQL 和 ORM 处理 Java 持久性的挫折。

它适用于任何可以用作键/值存储的数据库,例如FoundationDB(或任何 SQL 数据库,尽管所有内容都将被卡在单个键/值表中)。

于 2014-04-22T15:57:31.053 回答
1

听起来问题的核心问题在于关系模型本身。从所描述的情况来看,图形数据库将非常巧妙地映射问题域。作为替代方案,文档存储是解决问题的另一种方法,因为尽管阻抗仍然存在,但文档通常比集合更易于推理。当然,任何方法都会有自己的怪癖需要注意。

于 2013-08-03T04:36:44.577 回答
0

由于您想要一个简单轻量级的库并使用 SQL,我建议您看一下fjorm。它允许您毫不费力地使用 POJO 和 CRUD 操作。

免责声明:我是该项目的作者。

于 2013-09-13T20:24:06.703 回答
0

不知道是不是有点晚了。也许值得一试。这是库abacus-jdbc提供的解决方案:

Person person = Person.builder().firstName("jdbc").lastName("whoiam").build();
long personId = personDao.insert(person);

City city = new City(123, "Sunnyvalue");
int cityId = cityDao.insert(city);

Person person = Person.builder().firstName("jdbc").lastName("whoiam").build();
long personId = personDao.insert(person);

Address address = Address.builder().street("1130 Ky").cityId(cityId).state("CA").zipCode("95677").personId(personId).build();
addressDao.insert(address);

Person personFromDB = personDao.gett(personId);
System.out.println(personFromDB);

personDao.loadJoinEntitiesIfNull(personFromDB, Address.class);

System.out.println(personFromDB);

基本上你可以在不编写单个 DAL/DAO 方法的情况下进行 CRUD 和更多操作。您需要定义的所有 DAO 类是:

public interface PersonDao extends JdbcUtil.CrudDao<Person, Long, SQLBuilder.PSC, PersonDao>, 
                            JdbcUtil.JoinEntityHelper<Person, SQLBuilder.PSC, PersonDao> {
}

public interface AddressDao extends JdbcUtil.CrudDaoL<Address, SQLBuilder.PSC, AddressDao>, 
                                    JdbcUtil.JoinEntityHelper<Address, SQLBuilder.PSC, AddressDao> {
}

public interface CityDao extends JdbcUtil.CrudDao<City, Integer, SQLBuilder.PSC, CityDao> {
}

我知道它不能解决您所有的问题/问题。但至少它为您的几个问题提供了一些替代方法。如果您需要一个框架来提供您所要求的提取功能,毫无疑问,那就是 Hibernate。但有时,如果您想通过一些非常简单的方法来控制数据库操作,而不是依赖于框架。试试 abacus-jdbc这是Github上的所有示例代码

免责声明:我是abacus-jdbc的开发者

于 2020-07-21T05:40:31.163 回答