如何使用Hibernate / ORM保持清洁层分离?

时间:2011-11-04 16:25:46

标签: java hibernate java-ee jpa orm

如何使用Hibernate / ORM(或其他ORM ......)保持干净的图层?

清洁层分离的意思是保留Hibernate 层中的所有DAO内容。

例如,在创建一个大的CSV导出流时,我们应该经常做一些像evict这样的Hibernate操作来避免OutOfMemory ...输出流的填充属于视图,但是evict属于DAO

我的意思是我们不应该将evict操作放在前端/服务中,我们也不应该把业务逻辑放在DAO中......那么在这种情况下我们能做些什么呢?

在很多情况下,你必须做一些事情,比如逐出,冲洗,清除,刷新,特别是当你玩交易,大数据或类似的事情......

那么你如何使用像Hibernate这样的ORM工具来保持清晰的层分离?


编辑:我在工作中不喜欢的是我们有一个自定义的抽象DAO,允许服务将Hibernate标准作为参数。这是实用的,但对我来说理论上称这个DAO的服务不应该知道一个标准。我的意思是,我们不应该以任何方式将Hibernate内容导入业务/视图逻辑。


是否有答案,简单或其他?

11 个答案:

答案 0 :(得分:6)

如果“干净”是指上层不了解下层的实现,通常可以应用 Tell, don't ask原则。对于您的CSV流式传输示例,它应该是这样的,例如:

// This is a "global" API (meaning it is visible to all layers). This is ok as
// it is a specification and not an implementation.
public interface FooWriter {
    void write(Foo foo);
}

// DAO layer
public class FooDaoImpl {
    ...
    public void streamBigQueryTo(FooWriter fooWriter, ...) {
        ...
        for (Foo foo: executeQueryThatReturnsLotsOfFoos(...)) {
            fooWriter.write(foo);
            evict(foo);
        }
    } 
    ...
}

// UI layer
public class FooUI {
    ...
    public void dumpCsv(...) {
        ...
        fooBusiness.streamBigQueryTo(new CsvFooWriter(request.getOutputStream()), ...);
        ...
    }
}

// Business layer
public class FooBusinessImpl {
    ...
    public void streamBigQueryTo(FooWriter fooWriter, ...) {
        ...
        if (user.canQueryFoos()) {
            beginTransaction();
            fooDao.streamBigQueryTo(fooWriter, ...);
            auditAccess(...);
            endTransaction();
        }
        ...
    }
}

通过这种方式,您可以自由地处理您的特定ORM。这种“回调”方法的缺点是:如果您的图层位于不同的JVM上,那么它可能不太可行(在示例中,您需要能够序列化CsvFooWriter)。

关于通用DAO:我从未感到需要,我发现的大多数对象访问模式都不同,足以使特定的实现成为可取的。但是,确实进行层分离并强制业务层创建Hibernate标准是相互矛盾的路径。我会在DAO层为每个不同的查询指定一个不同的查询方法,然后我会让DAO实现以它可能选择的任何方式获得结果(条件,查询语言,原始SQL,......)。所以而不是:

public class FooDaoImpl extends AbstractDao<Foo> {
    ...
    public Collection<Foo> getByCriteria(Criteria criteria) {
        ...
    }
}

public class FooBusinessImpl {
    ...
    public void doSomethingWithFoosBetween(Date from, Date to) {
        ...
        Criteria criteria = ...;

        // Build your criteria to get only foos between from and to

        Collection<Foo> foos = fooDaoImpl.getByCriteria(criteria);
        ...
    }

    public void doSomethingWithActiveFoos() {
        ...
        Criteria criteria = ...;

        // Build your criteria to filter out passive foos

        Collection<Foo> foos = fooDaoImpl.getByCriteria(criteria);
        ...
    }
    ...
}

我愿意:

public class FooDaoImpl {
    ...
    public Collection<Foo> getFoosBetween(Date from ,Date to) {
        // build and execute query according to from and to
    }

    public Collection<Foo> getActiveFoos() {
        // build and execute query to get active foos
    }
}

public class FooBusinessImpl {
    ...
    public void doSomethingWithFoosBetween(Date from, Date to) {
        ...      
        Collection<Foo> foos = fooDaoImpl.getFoosBetween(from, to);
        ...
    }

    public void doSomethingWithActiveFoos() {
        ...
        Collection<Foo> foos = fooDaoImpl.getActiveFoos();
        ...
    }
    ...
}

虽然有人可能会认为我正在将一些业务逻辑推到DAO层,但对我来说这似乎是一种更好的方法:将ORM实现更改为替代实现会更容易。想象一下,例如,出于性能原因,您需要使用原始JDBC读取Foo来访问某些特定于供应商的扩展:使用通用DAO方法,您需要更改业务和DAO层。使用这种方法,您只需重新实现DAO层。

答案 1 :(得分:5)

嗯,你总是可以告诉你的DAO层做你想做的事情。在DAO层中使用类似cleanUpDatasourceCache的方法,或者类似的东西(甚至是不同对象的一组方法),对我来说并不是一种不好的做法。

然后,您的服务层就可以调用该方法,而无需考虑DAO所做的工作。使用直接JDBC调用的特定实现在该方法中不起作用。

答案 2 :(得分:4)

通常需要一个DAO层来包装数据访问逻辑。其他时候只是你想要用于CRUD操作的EntityManager,对于那些情况,我不会使用DAO,因为它会给代码增加不必要的复杂性。

How should EntityManager be used in a nicely decoupled service layer and data access layer?

答案 3 :(得分:2)

如果你不想将你的代码绑定到Hibernate,你可以通过JPA使用Hibernate,而不是过多地抽象你的DAO中的所有内容。除了替换Hibernate之外,你不太可能从JPA切换到其他东西。

答案 4 :(得分:2)

我的2美分:我认为层分离模式在大多数情况下都是很好的起点,但有一点我们必须逐个分析每个特定的应用程序并设计一个更灵活的解决方案。我的意思是,问自己例如:

  • 是你的DAO预计会在除了以外的其他环境中重复使用 导出csv数据?

  • 使用相同DAO的另一个实现是否有意义 没有休眠的界面?

如果两个答案都不是,那么持久性和数据表示之间的一点点耦合就可以了。我喜欢上面提出的回调解决方案。

恕我直言有时严格执行模式在可读性,可持续性等方面的成本较高,这是我们试图通过首先采用模式来解决的问题

答案 5 :(得分:1)

你可以通过实现DAO模式来实现层分离,并在Dao本身中完成所有与hibernate / JDBC / JPA相关的东西

例如:

您可以将Generic Dao界面指定为

public interface GenericDao <T, PK extends Serializable> {

/** Persist the newInstance object into database */
PK create(T newInstance);

/** Retrieve an object that was previously persisted to the database using
 *   the indicated id as primary key
 */
T read(PK id);

/** Save changes made to a persistent object.  */
void update(T transientObject);

/** Remove an object from persistent storage in the database */
void delete(T persistentObject);
}

及其实施

  public class GenericDaoHibernateImpl <T, PK extends Serializable>
implements GenericDao<T, PK>, FinderExecutor {
private Class<T> type;

public GenericDaoHibernateImpl(Class<T> type) {
    this.type = type;
}

public PK create(T o) {
    return (PK) getSession().save(o);
}

public T read(PK id) {
    return (T) getSession().get(type, id);
}

public void update(T o) {
    getSession().update(o);
}

public void delete(T o) {
    getSession().delete(o);
}

}

所以只要服务类调用任何Dao上的任何方法而不假设方法的内部实现

查看GenericDao链接

答案 6 :(得分:1)

Hibernate(作为SessionManager或JPA EntityManager)是DAO。就我所见,Repository模式是最佳起点。在image有一个很棒的DDD Sample Website结束,我认为这些内容可以说明你如何将事情分开。

我的应用程序图层具有显式业务操作或值的接口。业务规则位于域模型中,Hibernate等内容位于基础架构中。服务在域层定义为接口,在我的案例中在基础架构中实现。这意味着对于给定的Foo域对象(DDD术语中的聚合根),我通常从FooService获取 Foo,并且FooService与FooRepository进行对话,允许其人查找基于某些标准的Foo。该标准通过方法参数(可能是复杂的对象类型)表示,这些参数在实现方面(例如在HibernateFooRepository中)将被转换为HQL或Hibernate标准。

如果您需要批处理,它应该存在于应用程序级别并使用域服务来促进这一点。 StartBatchTransaction / EndBatchTransaction。 Hibernate可以监听开始/结束事件,以便协调清除,加载等等。

在序列化域实体的特定情况下,我认为采用一组标准并一次一个地迭代它们(从根实体)没有错。

我经常发现,在追求分离的过程中,我们经常试图让事情变得完整。它们不是同一个 - 你的应用程序必须做某事,并且可以而且应该明确地表达某些东西。

如果您可以替换以前使用HibernateFooRepository的InMemoryFooRepository,那么您就是在正确的道路上。当您坚持或至少尝试尊重我上面链接的图像中概述的分层时,自然流通单元和集成测试对象会鼓励这样做。

答案 7 :(得分:1)

你在这里得到了一些好的答案,我想补充一点我的想法(顺便说一句,这也是我们的代码中需要注意的事情)我还想关注Hibernate注释的问题/ JPA注释您可能需要在DAL之外使用的实体(即 - 在业务逻辑,甚至发送到您的客户端) -

A.如果对给定实体使用GenericDAO模式,您可能会发现您的实体使用Hibernate(或JPA注释)进行注释,例如@ Table,@ ManyToOne等等 - 这意味着您的客户端代码可能包含Hibernate / JPA注释,您需要一个合适的jar来编译它,或者在您的客户端代码上有一些其他支持,例如,如果您使用GWT作为您的客户端(可以支持JPA注释)为了获得实体编译),并在服务器和客户端代码之间共享实体,或者如果您编写一个Java客户端,使用InitialContext对Java应用程序服务器执行bean查找(在这种情况下,您将需要一个JAR
B.您可以使用的另一种方法是在服务器端使用Hibernate / JPA注释代码,并公开Web服务(比如说RESTFul Web服务或SOAP) - 这样,客户端使用不暴露知识的“接口”在Hibernate / JPA上(例如 - 在SOAP的情况下,WSDL定义服务的客户端和服务本身之间的契约)。
通过将体系结构分解为面向服务的体系结构,您可以获得各种好处,例如松耦合,易于更换代码片段,并且您可以将所有DAL逻辑集中在一个服务中,为其余服务提供服务,如果其他服务需要,以后自己更换DAL。

C.您可以使用“对象对象”映射框架(例如dozer)将带有Hibernate / JPA注释的类的对象映射到我称之为“真正的”POJO - 即 - 没有任何注释的Java bean。

D.最后关于注释 - 为什么要使用注释? Hibernate使用hbm xml文件作为执行“ORM魔术”的替代方法 - 这样您的类可以保持不带注释。

E.最后一点 - 我建议你看看我们在Ovirt所做的事情 - 你可以通过git clone我们的repo下载代码。您将在engine / backend / manager / modules / bll下找到 - 一个maven项目,包含我们的bll逻辑,并在engine / backend / manager / moduled / dal下 - 我们的DAL层(虽然目前用Spring-JDBC实现,有些是hibernate实验,你会得到一些关于如何在你的代码中使用它的好主意。我想补充一点,如果你去寻找类似的解决方案,我建议你在你的代码中注入DAO,而不是把它们放在像Singletone这样的我们使用getXXXDao方法(这是遗留代码,我们应该努力删除并转移到注入)。

答案 8 :(得分:0)

我建议您让数据库处理导出到CSV操作,而不是自己用Java构建它,它效率不高。 ORM不应该真正用于那些大规模的批处理操作,因为ORM只应该用于操纵事务数据。

大型Java批处理操作应该直接由JDBC完成,并关闭事务支持。

但是,如果您定期执行此操作,我建议您设置一个报告数据库,该数据库是应用程序未使用的数据库的延迟副本,并使用数据库可能附带的特定于数据库的复制工具。

您的解决方案架构师应该能够与其他小组合作,为您提供帮助。

如果您确实必须在应用程序层中执行此操作,那么使用原始JDBC调用可能是更好的选择。使用原始JDBC,您可以执行查询以在数据库端汇编所需的数据,并一次获取一行数据,然后写入输出流。

回答你的图层问题。虽然我不喜欢使用单词layer,因为它通常意味着一件事在另一件之上。我宁愿使用“组件”这个词,我有以下组件组。

<强>应用

  1. 域 - 只注释JPA类,没有持久性逻辑,通常是普通的JAR文件,但我建议只将它作为EJB中的包而不是处理类路径问题
  2. contract - 定义不同组件之间接口的WSDL和XSD文件,无论是Web服务还是UI。
  3. 事务脚本 - 无状态EJB,它们将事务和持久性单元注入其中并执行域对象的操作和持久性。这些可以实现合同生成的接口。
  4. UI - 一个单独的WAR项目,其中注入了EJB。
  5. <强>数据库

    1. O / R图 - 这是应用程序和数据团队商定的合同,以确保数据库将提供的MINIMUM。它不必显示所有内容。
    2. DDLs - 这是O / R图的数据库端实现,它将包含所有内容,但通常没有人应该关心,因为它的实现细节。
    3. 批量批处理操作,例如导出或复制
    4. 报告 - 提供查询以从系统获取业务价值报告。
    5. <强>遗留

      1. 消息传递合同 - 这些是消息传递系统使用的合同,例如JMS或WS-Notifications或标准Web服务。
      2. 他们的实施
      3. 转换脚本 - 用于将一个合同转换为另一个合同。

答案 9 :(得分:-1)

在我看来,我们需要再看看这些图层。 (如果我弄错了,我希望有人纠正我。)

  1. 前端/用户界面
  2. 商家
  3. 服务/ DAO
  4. 因此,对于生成报告的情况,这些层就像这样分解。

    1. 前端/用户界面
      • 将有一个带有“获取某些报告”按钮的用户界面
      • 然后该按钮将调用知道报告内容的业务层。
      • 报告生成器返回的数据在返回给用户之前会给出任何最终格式。
    2. 商家
      • MyReportGenerator.GenerateReportData()或类似名称将被称为
    3. 服务/ DAO
      • 将使用报告生成器DAO内部。 DAOLocator.GetDAO(Entity.class);或类似的工厂类型方法将用于获取DAO。返回的DAO将扩展Common DAO interface

答案 10 :(得分:-1)

好吧,为了清楚地分离关注点,或者你可以说清理层分离,你可以在你的应用程序中添加服务层,它位于你的FrontEnd和DaoLayer之间。

您可以使用Hibernate将业务逻辑放在Dao层中的Service层和数据库相关的东西中。

因此,如果您需要更改业务逻辑中的某些内容,则可以在不更改DAO的情况下编辑服务层,如果要更改Dao层,则可以不更改实际业务逻辑,即服务层。