Hibernate的StatelessSession - 它是什么以及如何使用它

87 阅读9分钟

获得我所有的视频课程,每月2次问答电话,每月的编码挑战,一个志同道合的开发者社区,以及定期的专家会议。

加入持久性中心!


Hibernate的一些核心功能是自动脏检查、冲刷和一级缓存。它们使大多数标准用例的实现变得简单而高效。但它们也增加了很多隐藏的复杂性,并不适合所有的用例。你的典型的夜间导入工作或其他大多数执行大量写操作的用例并不能从这些功能中受益。它们甚至常常会拖累这些用例。在这些情况下,Hibernate的StatelessSession 可能更适合。

内容

什么是StatelessSession

StatelessSession是Hibernate的一个专有功能,它提供了一个面向命令的API,更接近于JDBC。我将在本文中向你展示几个例子,说明如何使用它来实现典型的读写操作。但在我们仔细研究StatelessSession接口之前,我们需要谈谈与标准Session接口的概念上的区别。

Hibernate的StatelessSession 并不提供一级缓存、自动脏检查或写后自动化。它也不为你的托管关联提供懒惰加载,不使用第二层或查询缓存。任何通过StatelessSession 执行的操作也不会触发任何生命周期事件或拦截器。

与普通会话 或JPA的EntityManager 提供的所有自动功能不同,StatelessSession让你完全控制执行的SQL语句。如果你想从数据库中获取一些数据或初始化一个关联,你需要为它编写和执行一个查询。而如果你创建一个新的或改变一个现有的实体对象,你需要调用StatelessSession接口上的插入更新删除 方法来持久化你的改变。

这就要求你在持久化层的技术方面投入更多的思考。但如果你的用例不需要自动的脏检查、懒加载或一级缓存,使用StatelessSession也会大大减少Hibernate的性能开销。这使得Hibernate的StatelessSession 非常适用于导入或更新大量数据的用例。如果你已经在应用程序的其他部分使用了Hibernate,并且想重用你的实体模型,情况就更是如此了。

如果你需要获取许多不会改变的实体对象,并且不需要对相关实体进行任何懒惰的获取,你也可以尝试StatelessSession。但是返回用例特定的DTO投影的查询通常更适合这些用例。

如何使用StatelessSession

让我们使用一个StatelessSession 来读写实体对象。你可以通过与普通Session实例类似的方式获得一个StatelessSession实例。如果你的应用程序运行在应用服务器中或基于Spring,你可以简单地注入一个StatelessSession实例。而如果你使用的是普通的Hibernate,你可以在你的SessionFactory上调用openStatelessSession方法并使用它来启动一个事务。

StatelessSession statelessSession = sf.openStatelessSession();
statelessSession.getTransaction().begin();

// do something

statelessSession.getTransaction().commit();

在你得到一个StatelessSession 实例后,你可以用它来读写数据。

使用StatelessSession插入和更新实体

大多数项目使用StatelessSession 来插入或更新巨大的数据集。所以让我们从2个简单的写操作开始。

在使用StatelessSession 实现写操作时,你需要知道的最重要的方法是插入更新删除方法。如果你想持久化一个新的实体对象或者更新或删除一个现有的实体对象,你需要调用它们。在这样做的时候,请注意无状态会话不支持级联。所以,你需要为每一个你想持久化的实体对象触发你的写操作。

在下面的测试案例中,我想插入一个新的ChessPlayer实体对象,并在之后修复firstName 中的一个错字。

StatelessSession statelessSession = sf.openStatelessSession();
statelessSession.getTransaction().begin();

ChessPlayer player = new ChessPlayer();
player.setFirstName("Torben");
player.setLastName("Janssen");

log.info("Perform insert operation");
statelessSession.insert(player);

log.info("Update firstName");
player.setFirstName("Thorben");
statelessSession.update(player);

statelessSession.getTransaction().commit();

如果你熟悉Hibernate的Session 接口或JPA的EntityManager,你可能已经认识到主要的区别。我调用insert 方法来持久化新的ChessPlayer 对象,并调用update 方法来持久化改变后的firstName

正如我前面提到的,无状态会话 并不提供一级缓存、脏检查和自动写后优化。因此,当你用实体对象调用插入方法 时,Hibernate会立即执行一个SQL INSERT语句。而且,Hibernate并没有检测到改变了的firstName 属性。如果你想持久化这一变化,你需要调用更新方法。然后Hibernate会立即执行一个SQL UPDATE语句。

如果你使用我推荐的开发系统的日志配置,你可以在日志输出中看到所有这些。

17:46:23,963 INFO  [com.thorben.janssen.TestStatelessSession] - Perform insert operation
17:46:23,968 DEBUG [org.hibernate.SQL] - 
    select
        nextval('player_seq')
17:46:23,983 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        ChessPlayer
        (birthDate, firstName, lastName, version, id) 
    values
        (?, ?, ?, ?, ?)
17:46:23,988 INFO  [com.thorben.janssen.TestStatelessSession] - Update firstName
17:46:23,989 DEBUG [org.hibernate.SQL] - 
    update
        ChessPlayer 
    set
        birthDate=?,
        firstName=?,
        lastName=?,
        version=? 
    where
        id=? 
        and version=?

正如你在这个例子中看到的,没有一级缓存、自动脏检查和刷新操作,需要你触发所有的数据库交互。这让你完全控制了SQL语句的执行,并且在编写巨大的数据集时提供了更好的性能。

使用StatelessSession读取实体对象

当你使用StatelessSession从数据库中读取实体对象时,你的代码看起来与使用标准Session的相同。但是你需要知道一些重要的Hibernate内部的差异。

我在前面提到,StatelessSession并不提供懒惰加载。因此,在从数据库中获取实体对象时,你需要初始化所有需要的关联。否则,当你第一次访问关联时,Hibernate会抛出一个*LazyInitializationException* 。初始化关联的最好方法是使用EntityGraph或者在你的JPQL查询中包含一个JOIN FETCH子句。

在下面的例子中,我使用一个带有2个JOIN FETCH子句的JPQL查询来加载一个ChessPlayer 实体对象。JOIN FETCH子句告诉Hibernate将关联初始化为他们用白棋和黑棋进行的游戏。

StatelessSession statelessSession = sf.openStatelessSession();
statelessSession.getTransaction().begin();

ChessPlayer player = statelessSession.createQuery("""
											SELECT p 
											FROM ChessPlayer p 
												JOIN FETCH p.gamesWhite 
												JOIN FETCH p.gamesBlack 
											WHERE p.id=:id""", ChessPlayer.class)
									 .setParameter("id", 1L)
									 .getSingleResult();

log.info(player.getFirstName() + " " + player.getLastName());
log.info("White pieces: " + player.getGamesWhite().size());
log.info("Black pieces: " + player.getGamesBlack().size());

statelessSession.getTransaction().commit();

如前所述,使用StatelessSession 实现的读取操作*,* 与Session实例之间的差异在你的代码中并不直接可见。而对于日志输出也是如此。

17:58:09,648 DEBUG [org.hibernate.SQL] - 
    select
        c1_0.id,
        c1_0.birthDate,
        c1_0.firstName,
        g2_0.playerBlack_id,
        g2_0.id,
        g2_0.chessTournament_id,
        g2_0.date,
        g2_0.playerWhite_id,
        g2_0.round,
        g2_0.version,
        g1_0.playerWhite_id,
        g1_0.id,
        g1_0.chessTournament_id,
        g1_0.date,
        g1_0.playerBlack_id,
        g1_0.round,
        g1_0.version,
        c1_0.lastName,
        c1_0.version 
    from
        ChessPlayer c1_0 
    join
        ChessGame g1_0 
            on c1_0.id=g1_0.playerWhite_id 
    join
        ChessGame g2_0 
            on c1_0.id=g2_0.playerBlack_id 
    where
        c1_0.id=?
17:58:09,682 DEBUG [org.hibernate.stat.internal.StatisticsImpl] - HHH000117: HQL: SELECT p
FROM ChessPlayer p
    JOIN FETCH p.gamesWhite
    JOIN FETCH p.gamesBlack
WHERE p.id=:id, time: 56ms, rows: 1
17:58:09,685 INFO  [com.thorben.janssen.TestStatelessSession] - Magnus Carlsen
17:58:09,685 INFO  [com.thorben.janssen.TestStatelessSession] - White pieces: 1
17:58:09,686 INFO  [com.thorben.janssen.TestStatelessSession] - Black pieces: 2

但是有一些重要的内部差异。Hibernate不仅不支持StatelessSessions的懒惰加载,而且也不使用任何缓存,包括一级缓存。这就减少了每次数据库查询所需的开销。但是,如果你在同一个会话中多次读取同一个实体,Hibernate就不能再保证你总是得到同一个对象。

你可以在下面的测试案例中看到,在这个案例中我执行了两次相同的查询。

StatelessSession statelessSession = sf.openStatelessSession();
statelessSession.getTransaction().begin();

ChessPlayer player1 = statelessSession.createQuery("""
											SELECT p 
											FROM ChessPlayer p 
												JOIN FETCH p.gamesWhite 
												JOIN FETCH p.gamesBlack 
											WHERE p.id=:id""", ChessPlayer.class)
									  .setParameter("id", 1L)
									  .getSingleResult();
ChessPlayer player2 = statelessSession.createQuery("""
											SELECT p 
											FROM ChessPlayer p 
												JOIN FETCH p.gamesWhite 
												JOIN FETCH p.gamesBlack 
											WHERE p.id=:id""", ChessPlayer.class)
									  .setParameter("id", 1L)
									  .getSingleResult();

assertNotEquals(player1, player2);

statelessSession.getTransaction().commit();

使用一个标准的Session 实例,Hibernate将执行第一次查询,为返回的记录实例化一个实体对象,并将其存储在第一级缓存中。之后,它将执行第二次查询,检查第一级缓存中代表结果集中返回的记录的实体对象,并返回该对象。这可以确保你在同一会话中多次获取数据库记录时,总是得到相同的实体对象。

如果没有第一层缓存,StatelessSession就不知道任何先前选择的实体对象。它必须为查询返回的每一条记录实例化一个新的对象。由于这个原因,你可以得到多个代表同一数据库记录的对象。在前面的例子中,player1player2对象就是这种情况。

请在编写你的业务代码时记住这一点,并确保你总是使用相同的实体对象进行写操作。否则,你可能会覆盖之前执行的更改。

总结

Hibernate的StatelessSession 接口提供了一个面向命令的API,让你对执行的SQL语句有更多控制。它更接近于JDBC,不支持任何缓存、自动刷新、脏检查、级联和懒加载。

这使得StatelessSession非常适用于所有不受益于这些功能的用例。典型的例子是批处理作业或其他执行许多简单写操作的用例。

但如果没有所有这些特性,实现你的持久层需要更多的工作。你需要自己触发所有的数据库操作。例如,在你改变一个或多个实体属性后,你需要在你的StatelessSession 实例上调用更新 方法,以在数据库中持久化该变化。否则,Hibernate将不知道这个变化,也不会触发任何SQL语句。

当你从数据库中获取一个实体对象时,你还需要初始化所有需要的关联。而且你需要注意的是,如果你多次获取同一记录,StatelessSession不会返回相同的实体对象。这使得查询结果的处理变得有些复杂。

总的来说,如果你想减少Hibernate的会话处理的开销,并且不需要懒惰加载、级联、一级缓存和自动刷新等功能,那么Hibernate的StatelessSession 是一个很好的功能。