具有一对多和多对多关系的 JOOQ pojos
Posted
技术标签:
【中文标题】具有一对多和多对多关系的 JOOQ pojos【英文标题】:JOOQ pojos with one-to-many and many-to-many relations 【发布时间】:2014-06-13 07:02:36 【问题描述】:我很难理解如何处理与 JOOQ 的一对多和多对多关系的 pojo。
我存储玩家创建的位置(一对多关系)。一个位置可以容纳多个可能访问它的其他玩家(多对多)。数据库布局归结为以下内容:
CREATE TABLE IF NOT EXISTS `Player` (
`player-id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`player` BINARY(16) NOT NULL,
PRIMARY KEY (`player-id`),
UNIQUE INDEX `U_player` (`player` ASC))
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS `Location` (
`location-id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(32) CHARACTER SET 'utf8' COLLATE 'utf8_bin' NOT NULL,
`player-id` INT UNSIGNED NOT NULL COMMENT '
UNIQUE INDEX `U_name` (`name` ASC),
PRIMARY KEY (`location-id`),
INDEX `Location_Player_fk` (`player-id` ASC),
CONSTRAINT `fk_location_players1`
FOREIGN KEY (`player-id`)
REFERENCES `Player` (`player-id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS `location2player` (
`location-id` INT UNSIGNED NOT NULL,
`player-id` INT UNSIGNED NOT NULL,
INDEX `fk_ location2player_Location1_idx` (`location-id` ASC),
INDEX `fk_location2player_Player1_idx` (`player-id` ASC),
CONSTRAINT `fk_location2player_Location1`
FOREIGN KEY (`location-id`)
REFERENCES `Location` (`location-id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_location2player_Player1`
FOREIGN KEY (`player-id`)
REFERENCES `Player` (`player-id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
在我的 java 应用程序中,所有这些信息都存储在一个 pojo 中。请注意,玩家和受邀玩家列表可以在应用程序中更新,也需要在数据库中更新:
public class Location
private final String name;
private UUID player;
private List<UUID> invitedPlayers;
public void setPlayer(UUID player)
this.player = player;
public void invitePlayer(UUID player)
invitedPlayers.add(player);
public void uninvitePlayer(UUID player)
invitedPlayers.remove(player);
//additional methods…
我可以使用 JOOQ 的 pojo 映射将这三个记录映射到单个 pojo 中吗?我可以从这个 pojo 中使用 JOOQ 的 CRUD 功能来更新一对多和多对多的关系吗?如果不能使用 pojo 映射,除了使用它来编写我的 SQL 语句之外,我可以以任何方式利用 JOOQ 吗?
【问题讨论】:
【参考方案1】:将MULTISET
用于jOOQ 3.15 的嵌套集合
从 jOOQ 3.15 开始,您可以使用standard SQL MULTISET
operator 来嵌套集合,并抽象以下 SQL/XML 或 SQL/JSON 序列化格式。您的查询将如下所示:
List<Location> locations
ctx.select(
LOCATION.NAME,
LOCATION.PLAYER,
multiset(
select(LOCATION2PLAYER.PLAYER_ID)
.from(LOCATION2PLAYER)
.where(LOCATION2PLAYER.LOCATION_ID.eq(LOCATION.LOCATION_ID))
).as("invitedPlayers")
)
.from(LOCATION)
.fetchInto(Location.class);
如果您的 DTO 是不可变的(例如 Java 16 记录),您甚至可以避免使用反射进行映射,并使用构造函数引用和 new jOOQ 3.15 ad-hoc conversion feature 将类型安全地映射到 DTO 构造函数中。
List<Location> locations
ctx.select(
LOCATION.NAME,
LOCATION.PLAYER,
multiset(
select(LOCATION2PLAYER.PLAYER_ID)
.from(LOCATION2PLAYER)
.where(LOCATION2PLAYER.LOCATION_ID.eq(LOCATION.LOCATION_ID))
).as("invitedPlayers").convertFrom(r -> r.map(Record1::value1))
)
.from(LOCATION)
.fetch(Records.mapping(Location::new));
See also this blog post for more details about MULTISET
将 SQL/XML 或 SQL/JSON 用于 jOOQ 3.14 的嵌套集合
从 jOOQ 3.14 开始,如果您的 RDBMS 支持,可以使用 SQL/XML 或 SQL/JSON 嵌套集合。然后,您可以使用 Jackson、Gson 或 JAXB 从文本格式映射回您的 Java 类。例如:
List<Location> locations
ctx.select(
LOCATION.NAME,
LOCATION.PLAYER,
field(
select(jsonArrayAgg(LOCATION2PLAYER.PLAYER_ID))
.from(LOCATION2PLAYER)
.where(LOCATION2PLAYER.LOCATION_ID.eq(LOCATION.LOCATION_ID))
).as("invitedPlayers")
.convertFrom(r -> r.map(Records.mapping(Pla)
)
.from(LOCATION)
.fetch(Records.mapping(Location::new));
在某些数据库产品中,例如 PostgreSQL,您甚至可以使用ARRAY_AGG()
来使用 SQL 数组类型,并跳过使用中间 XML 或 JSON 格式。
请注意,JSON_ARRAYAGG()
将空集聚合到 NULL
,而不是空集 []
。 If that's a problem, use COALESCE()
历史答案(jOOQ 3.14 之前)
jOOQ 还没有开箱即用地进行这种 POJO 映射,但您可以利用 ModelMapper 之类的东西,它具有专用的 jOOQ integration,在一定程度上适用于这些场景。
本质上,ModelMapper 挂钩到 jOOQ 的 RecordMapper
API。更多细节在这里:
【讨论】:
谢谢。但是我如何将多对多数据提取到 JOOQ 的逻辑中呢?在 JDBC 中,我将使用JOIN
子句获取数据并将每个附加行添加到 pojo 中的集合中,但是使用 JOOQ,RecordMapper 无法访问以前的对象。在创建相应对象时,我是否希望在RecordMapper
中对多对多关系执行另一个查询?还是有其他方法可以做到这一点?
是的,RecordMapper
是一个函数式接口,因此将“有状态”的东西作为分组算法来实现并不容易。但我们也有各种Result.intoGroups()
。也许,这些会有帮助?另一种选择是利用像ModelMapper 这样的东西,它本机支持jOOQ。在任何情况下,您都不应该仅仅因为 ResultSet 到 Object 的映射算法变得有点复杂而执行两个查询。
在其他answers(以及您的blog)中,您提到Java 8 的流对于将数据库中的复杂关系正确匹配到POJO 中很有用。从这个角度来说,Streams可以用来解决这个问题吗?
@thee:是的,他们当然可以。甚至比使用 Streams 更容易,您可以使用 jOOλ。我在这里的回答并不详尽。我会在不久的将来添加更多示例,感谢您指出这一点。
@thee:您是否有兴趣在用户组中更详细地说明您的用例? groups.google.com/forum/#!forum/jooq-user。如果我知道您的具体期望,也许我可以比这里更好地回答您的问题。当然,您可以使用我提到的工具进行这些映射,但很难说(并在 Stack Overflow 上讨论),这对您来说是否足够好。【参考方案2】:
您可以使用SimpleFlatMapper 查询的结果集。
创建一个以玩家为键的映射器
JdbcMapper<Location> jdbcMapper =
JdbcMapperFactory.addKeys("player").newMapper(Location.class);
然后使用 fetchResultSet 获取 ResultSet 并将其传递给映射器。 请注意,orderBy(LOCATION.PLAYER_ID) 很重要,否则您可能会得到拆分位置。
try (ResultSet rs =
dsl
.select(
LOCATION.NAME.as("name"),
LOCATION.PLAYER_ID.as("player"),
LOCATION2PLAYER.PLAYERID.as("invited_players_player"))
.from(LOCATION)
.leftOuterJoin(LOCATION2PLAYER)
.on(LOCATION2PLAYER.LOCATION_ID.eq(LOCATION.LOCATION_ID))
.orderBy(LOCATION.PLAYER_ID)
.fetchResultSet())
Stream<Location> stream = jdbcMapper.stream(rs);
然后在流上做你需要做的,你也可以得到一个迭代器。
【讨论】:
重要的是要注意这个解决方案绕过了 Jooq 的记录映射器并直接进入结果集。如果您有任何 Jooq 力类型,这将是有问题的 - 您必须为 SFM 重新定义它们 我的理解是强制类型是为了生成Record对象。您可以使用 sfm 使用强制类型映射到 jOOQ 记录对象。那么问题是如何将连接映射到 Record 对象?您将需要使用 Tuple2以上是关于具有一对多和多对多关系的 JOOQ pojos的主要内容,如果未能解决你的问题,请参考以下文章