实体、聚合和聚合根是Spring Data JDBC使用的一些核心概念。基于它们,Spring Data JDBC决定将哪些对象加载或持久化在一起。它们还定义了你可以建立哪种关联的模型。这表明理解这两个概念以及它们如何一起工作是多么重要。
Spring Data JPA并没有发明实体、聚合体和聚合根的概念。它们是由领域驱动设计定义的。一个实体是一个有id的领域对象,它可以有多个附加属性。一个可以被视为单一单位的实体集群被称为聚合。而聚合根是聚合的根元素。聚合根是被聚合外部引用的对象,它引用同一聚合中的其他实体。正如你在下图的例子中看到的,一个典型的聚合结构看起来像一棵树,聚合根是它的根。
Spring Data JDBC在设计时就考虑到了这些概念。你应该为每个聚合体建模一个存储库。存储库在从数据库中获取聚合信息或持久化任何变化时,将聚合信息作为一个单元来处理。
听起来很简单,对吗?
那么,将聚合体作为一个单元来处理有一些副作用,你应该知道。如果你在以前的项目中使用了Spring Data JPA,你可能会发现其中的一些问题令人困惑。但是别担心,这一切都不复杂,你会很快习惯的。
聚合体的建模
正如我前面提到的,一个聚合体被视为一个单一的单元,由一个或多个实体组成。其中一个实体是聚合根,它从外部被引用,并引用聚合中的其他实体。
这些听起来都不特别,你可能想知道我为什么要重复这一切。原因很简单,基于这样的描述,你不需要多对多的关联、多对一的关联,或者任何一般的双向关联。这就是为什么Spring Data JDBC不支持它们。
如果你在以前的项目中使用过Spring Data JPA,这可能会让你吃惊。但是你可以对你的领域进行建模并遵循这些约束。你的模型符合领域驱动设计的概念,而避免这些关联会让一些事情变得更容易。
让我们仔细看看ChessGame 聚合,这样我就可以告诉你,你可以在没有这些关联的情况下为聚合建模。ChessGame 集合由ChessGame 和ChessMove这两个实体组成。ChessGame 实体是ChessGame 集合的根。
public class ChessGame {
@Id
private Long id;
private LocalDateTime playedOn;
private AggregateReference<ChessPlayer, Long> playerWhite;
private AggregateReference<ChessPlayer, Long> playerBlack;
private List<ChessMove> moves = new ArrayList<>();
...
}
正如你所看到的,该 国际象棋游戏 实体对ChessMove 实体类建立了一对多的关联。但该 国际象棋运动实体并没有对其聚合根的引用进行建模。如果你需要获得下了某一步棋的棋谱,你需要执行一个查询。我在《使用Spring Data JDBC的自定义查询和预测指南》中解释了如何定义这种查询。
public class ChessMove {
private Integer moveNumber;
private MoveColor color;
private String move;
...
}
引用其他聚合体
每个ChessGame是由2个玩家进行的。我将ChessPlayer建模为一个单独的聚合体,因为玩家是独立于游戏或移动的。
ChessPlayer实体类为一个棋手建模,是ChessPlayer 聚合体的唯一类。由于这个原因,它也是聚合的根。
在领域驱动设计中,与不同聚合体的关联被建模为相关聚合体的id引用。当使用Spring Data JDBC时,你可以使用AggregateReference接口进行建模。我在ChessGame 实体类中使用它来模拟对下白棋的玩家和下黑棋的玩家的引用。
public class ChessGame {
private AggregateReference<ChessPlayer, Long> playerWhite;
private AggregateReference<ChessPlayer, Long> playerBlack;
...
}
当获取ChessGame对象时,Spring Data JDBC使用存储在数据库中的外键值来初始化每个AggregateReference。但是与其他ORM框架(例如Hibernate或Spring Data JPA)相比,Spring Data JDBC不能自动获取被引用的实体对象。
为了获取被引用的ChessPlayer,你需要使用ChessPlayerRepository从数据库中获取它。这让你可以完全控制执行的SQL语句,并避免了你可能从其他ORM框架中知道的懒惰加载问题。
为一个聚合体建立存储库的模型
在你建立了一个聚合的模型后,你可以为它定义一个资源库。如前所述,一个聚合体被当作一个单元来处理。这意味着你可以读取并持久化整个聚合,所有需要的操作都被当作一个原子操作来处理。因此,每个聚合体应该只有一个资源库。这个存储库处理整个聚合体及其所有实体的所有数据库操作。
你可以用定义其他Spring Data资源库的相同方式来定义Spring Data JDBC资源库。你定义了一个扩展了Spring Data JDBC标准存储库接口之一的接口,例如CrudRepository接口。然后Spring Data JDBC为你提供该接口的实现和一组标准操作。在CrudRepository的例子中,这些方法是用来持久化、更新、删除和读取聚合。如果你需要额外的查询或其他功能,你可以在你的接口定义中添加所需的方法。
public interface ChessGameRepository extends CrudRepository<ChessGame, Long> {
List<ChessGame> findByPlayedOn(LocalDateTime playedOn);
List<ChessGame> findByPlayedOnIsBefore(LocalDateTime playedOn);
int countByPlayedOn(LocalDateTime playedOn);
List<ChessGame> findByPlayerBlack(AggregateReference<ChessPlayer, Long> playerBlack);
List<ChessGame> findByPlayerBlack(ChessPlayer playerBlack);
}
在本文的范围内,我希望你能熟悉Spring Data的存储库接口及其派生查询功能。如果你不熟悉,请阅读我的指南,用Spring Data JDBC定义自定义查询和预测。
尽管我在之前的文章中解释了存储库及其查询功能,但我仍需要向你展示一些东西,以解释Spring Data JDBC对聚合的处理的意义。
读取一个聚合体
因为Spring Data JDBC将聚合体作为一个单元来处理,所以它总是获取整个聚合体及其所有实体。如果你的聚合体由多个实体和多个一对多的关联组成,那就会出现问题。
让我们调用ChessGameRepository 的findById 方法,检查执行的SQL语句。
gameRepo.findById(gameId);
该 ChessGameRepository 返回ChessGame的聚合。聚合由一个 国际象棋游戏 实体和一个List ofChessMove 实体。正如你在日志输出中看到的,Spring Data JDBC执行了2条SQL语句。第一条语句获取了 国际象棋游戏 实体,第二条是获取游戏中的所有ChessMoves。
2022-07-05 18:33:05.328 DEBUG 8676 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL query
2022-07-05 18:33:05.329 DEBUG 8676 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [SELECT "chess_game"."id" AS "id", "chess_game"."played_on" AS "played_on", "chess_game"."player_black" AS "player_black", "chess_game"."player_white" AS "player_white" FROM "chess_game" WHERE "chess_game"."id" = ?]
2022-07-05 18:33:05.345 DEBUG 8676 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL query
2022-07-05 18:33:05.345 DEBUG 8676 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [SELECT "chess_move"."move" AS "move", "chess_move"."color" AS "color", "chess_move"."move_number" AS "move_number", "chess_move"."chess_game_key" AS "chess_game_key" FROM "chess_move" WHERE "chess_move"."chess_game" = ? ORDER BY "chess_game_key"]
在这个例子中,获取整个 棋牌游戏 集合的性能影响很小。但是,如果你获取了多个聚合,或者你的聚合变得更加复杂,包括更多的实体和to-many关联,这种情况很快就会改变。
为了避免性能问题,你应该尽可能地保持你的聚合体小而简洁。因此,如果你看到有机会将某些东西建模为一个单独的聚合,那么这样做往往是个好主意。
持久化和更新聚合
Spring Data JDBC不仅在从数据库中获取聚合时将其视为一个单元。在持久化一个新的或更新一个现有的实体时,它也是这样做的。
持久一个聚合体很容易
这使得持久化一个新的聚合体非常容易。你只需要实例化你的聚合,并向存储库的保存方法提供聚合根。然后,Spring Data JDBC将自动持久化属于该聚合的所有实体。
在下面的测试案例中,我使用了这个方法来持久化一个新的 国际象棋游戏 聚合。我实例化了一个新的 国际象棋游戏 对象,它是聚合的根。然后我实例化了4个ChessMoves,并将它们添加到游戏中的棋步列表 中。在最后一步,我调用ChessGameRepository 的保存方法,只提供我的 国际象棋游戏 对象。
ChessMove white1 = new ChessMove();
white1.setColor(MoveColor.WHITE);
white1.setMoveNumber(1);
white1.setMove("e4");
ChessMove black1 = new ChessMove();
black1.setColor(MoveColor.BLACK);
black1.setMoveNumber(2);
black1.setMove("e5");
ChessMove white2 = new ChessMove();
white2.setColor(MoveColor.WHITE);
white2.setMoveNumber(2);
white2.setMove("Nf3");
ChessMove black2 = new ChessMove();
black2.setColor(MoveColor.BLACK);
black2.setMoveNumber(2);
black2.setMove("Nc6");
ChessGame game = new ChessGame();
game.setPlayedOn(LocalDateTime.now());
game.setMoves(Arrays.asList(white1, black1, white2, black2));
gameRepo.save(game);
正如你在日志输出中看到的,Spring Data JDBC执行了5条SQL INSERT语句来持久化整个聚合。它首先向chess_game 表写入1条记录,然后向chess_move 表写入4条记录。
2022-07-05 18:36:03.474 DEBUG 28416 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL update and returning generated keys
2022-07-05 18:36:03.475 DEBUG 28416 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [INSERT INTO "chess_game" ("played_on", "player_black", "player_white") VALUES (?, ?, ?)]
2022-07-05 18:36:03.503 DEBUG 28416 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL update and returning generated keys
2022-07-05 18:36:03.503 DEBUG 28416 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [INSERT INTO "chess_move" ("chess_game", "chess_game_key", "color", "move", "move_number") VALUES (?, ?, ?, ?, ?)]
2022-07-05 18:36:03.510 DEBUG 28416 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL update and returning generated keys
2022-07-05 18:36:03.511 DEBUG 28416 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [INSERT INTO "chess_move" ("chess_game", "chess_game_key", "color", "move", "move_number") VALUES (?, ?, ?, ?, ?)]
2022-07-05 18:36:03.515 DEBUG 28416 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL update and returning generated keys
2022-07-05 18:36:03.515 DEBUG 28416 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [INSERT INTO "chess_move" ("chess_game", "chess_game_key", "color", "move", "move_number") VALUES (?, ?, ?, ?, ?)]
2022-07-05 18:36:03.519 DEBUG 28416 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL update and returning generated keys
2022-07-05 18:36:03.519 DEBUG 28416 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [INSERT INTO "chess_move" ("chess_game", "chess_game_key", "color", "move", "move_number") VALUES (?, ?, ?, ?, ?)]
更新一个聚合体可能是低效的
尽管持久化一个聚合体很舒服,但作为一个单元的处理使得更新操作的效率很低。让我们运行下面的测试案例,我获取了一个ChessGame 对象,在告诉Spring Data JDBC保存该对象之前,我只改变了playedOn属性的值。
ChessGame game = gameRepo.findById(gameId).orElseThrow();
game.setPlayedOn(LocalDateTime.now());
gameRepo.save(game);
Spring Data JDBC将聚合体视为一个单元,并不跟踪它从数据库中获取的数据。由于这个原因,它无法检测到聚合体的哪一部分发生了变化。这对于每一个多对多的关联来说都是一个问题。
在这个例子中,Spring Data JDBC不知道是否或哪个 国际象棋运动 对象发生了变化。由于这个原因,它必须要替换所有的对象。
正如你在日志输出中看到的,它更新了表中的记录,从 国际象棋游戏 表中的记录,从ChessMove 表中删除所有记录,并为每个 国际象棋运动 对象。
2022-07-05 18:38:52.927 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL query
2022-07-05 18:38:52.928 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [SELECT "chess_game"."id" AS "id", "chess_game"."played_on" AS "played_on", "chess_game"."player_black" AS "player_black", "chess_game"."player_white" AS "player_white" FROM "chess_game" WHERE "chess_game"."id" = ?]
2022-07-05 18:38:52.945 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL query
2022-07-05 18:38:52.946 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [SELECT "chess_move"."move" AS "move", "chess_move"."color" AS "color", "chess_move"."move_number" AS "move_number", "chess_move"."chess_game_key" AS "chess_game_key" FROM "chess_move" WHERE "chess_move"."chess_game" = ? ORDER BY "chess_game_key"]
2022-07-05 18:38:52.972 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
2022-07-05 18:38:52.973 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [UPDATE "chess_game" SET "played_on" = ?, "player_black" = ?, "player_white" = ? WHERE "chess_game"."id" = ?]
2022-07-05 18:38:52.987 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
2022-07-05 18:38:52.987 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [DELETE FROM "chess_move" WHERE "chess_move"."chess_game" = ?]
2022-07-05 18:38:52.993 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL update and returning generated keys
2022-07-05 18:38:52.994 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [INSERT INTO "chess_move" ("chess_game", "chess_game_key", "color", "move", "move_number") VALUES (?, ?, ?, ?, ?)]
2022-07-05 18:38:53.000 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL update and returning generated keys
2022-07-05 18:38:53.000 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [INSERT INTO "chess_move" ("chess_game", "chess_game_key", "color", "move", "move_number") VALUES (?, ?, ?, ?, ?)]
2022-07-05 18:38:53.005 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL update and returning generated keys
2022-07-05 18:38:53.005 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [INSERT INTO "chess_move" ("chess_game", "chess_game_key", "color", "move", "move_number") VALUES (?, ?, ?, ?, ?)]
2022-07-05 18:38:53.010 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL update and returning generated keys
2022-07-05 18:38:53.010 DEBUG 34968 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [INSERT INTO "chess_move" ("chess_game", "chess_game_key", "color", "move", "move_number") VALUES (?, ?, ?, ?, ?)]
根据你的聚合体的大小和复杂性,这种对更新操作的处理会导致严重的性能问题。避免这些问题的最好方法是保持你的聚合体小而简练。
结论
聚合体是一组被视为一个单元的实体对象。正如你在这篇文章中所看到的,这使得一些操作变得更容易。例如,你可以很容易地持久化整个聚合体,而且你不必担心LazyInitializationExceptions,你可能从其他ORM中知道这个问题。
但是,如果Spring Data JDBC不得不从数据库中获取过多的记录,或者不得不替换实体的列表,那么将聚合体作为一个单元来处理也会带来性能问题。为了使这些影响尽可能小,我建议保持你的聚合的简洁和简单。你的聚合包括的关联和实体越少,出现性能问题的风险就越低。所以,如果你有机会将一些东西建模为多个小聚合体,你应该这样做。