使用Spring JPA规范和Hibernate连接不同元素

15

我正在尝试构建一个JPA规范,用于实现I18N以便能够过滤名称。
在数据库中,我有多种语言的翻译。
问题是大多数情况下仅指定默认语言。
因此,当有人请求非默认语言的翻译时,我希望它能回退到默认语言。

到目前为止,我已经能够在规范中构建这个。

Specification<S> specification = (root, query, cb) -> {
  Join i18n = root.join("translations", JoinType.LEFT);
  i18n.alias("i18n");
  Join i18nDefault = root.join("translations", JoinType.LEFT);
  i18nDefault.alias("i18nDefault");

  i18n.on(cb.and(cb.equal(i18n.get("itemId"), root.get("itemId")), cb.equal(i18n.get("languageId"), 1)));
  i18nDefault.on(cb.and(cb.equal(i18nDefault.get("itemId"), root.get("itemId")), cb.equal(i18nDefault.get("languageId"), 22)));

  // Clauses and return stuff
};

但是这会导致一个错误,听起来对这个解决方案来说不是好消息。
org.hibernate.hql.internal.ast.QuerySyntaxException: with-clause referenced two different from-clause elements
[
 select generatedAlias0 from com.something.Item as generatedAlias0 
 left join generatedAlias0.i18n as i18n with (i18n.itemId=generatedAlias0.itemId) and ( i18n.languageId=1L )
 left join generatedAlias0.i18n as i18nDefault with (i18nDefault.itemId=generatedAlias0.itemId) and ( i18nDefault.languageId=1L )
];

因此,Hibernate不允许我使用不同的元素(在本例中是itemId和languageId)构建with子句。
有没有什么方法可以正确实现或以不同的方式实现?

最终,我希望Hibernate生成一个类似于以下内容的查询(Oracle)

SELECT *
FROM item i
LEFT JOIN i18n_item i18n ON i.item_id = i18n.item_id AND i18n.language_id = 22
LEFT JOIN i18n_item i18n_default ON i.item_id = i18n_default.item_id AND i18n_default.language_id = 1
// where-clauses;


使用where子句解决此问题


我看到了一些其他相关的问题,它们的答案都说要使用where子句而不是第二个连接条件。 问题在于,非默认语言的翻译可能会缺少某些记录的内容。由于用户是指定其内容的翻译者(他们也管理)。

例如,用户可能已经为英语专用的项目指定了200条翻译记录,但仅为荷兰语专用的项目指定了190条翻译记录。在上面的示例中,英语是默认语言。

我尝试使用where子句构建查询,但这导致结果数量不一致。我的意思是未过滤的结果将是200个项目。当我使用like '%a%'过滤名称时,它会返回150个结果,而当我翻转过滤器(not like '%a%')时,它会返回大约30个结果。我希望它们加起来等于200。

这个方法可行

SELECT *
FROM item i
LEFT JOIN i18n_item i18n ON i.item_id = i18n.item_id AND i18n.language_id = 22
LEFT JOIN i18n_item i18n_default ON i.item_id = i18n_default.item_id AND i18n_default.language_id = 1
WHERE 
(
  (lower(i18n.name) not like '%a%') 
or 
  (i18n.name is null and (lower(i18n_default.name) not like '%a%'))
)

这个不起作用(没有返回正确数量的元素)

SELECT *
FROM item i
LEFT JOIN i18n_item i18n ON i.item_id = i18n.item_id
LEFT JOIN i18n_item i18n_default ON i.item_id = i18n_default.item_id
WHERE 
(
  (i18n.language_id = 22 and lower(i18n.name) not like '%a%') 
or 
  (i18n_default.language_id = 1 and i18n.name is null and (lower(i18n_default.name) not like '%a%'))
)

有没有通过JPA规范实现有效查询的方法?


我注意到这个票据增加了对显式连接的支持,这正是我所需要的:https://hibernate.atlassian.net/browse/HHH-16。就我所知,它从Hibernate 5.1.0开始得到支持。 - Robin Hermans
1个回答

12

经过一个周末的研究,我发现 Hibernate 5.1.0 带来了一项新功能,允许您使用多个列/字段的条件构建连接。这被称为 cross-join(请参见注释)。

请参阅 此工单此提交记录

我在 Spring Data Gitter 上询问了这将何时可用于 Spring Boot 1.x 的事情。他们告诉我要在我的 maven 或 gradle 文件中设置 hibernate.version 属性。因此,这似乎仅适用于使用 org.springframework.boot gradle 插件的情况。

apply plugin: "org.springframework.boot"

由于我使用Gradle,因此最终需要将以下内容添加到我的build.gradle文件中

ext {
    set('hibernate.version', '5.1.0.Final')
}

如果使用 gradle.properties 文件,你只需要在文件中添加这一行

hibernate.version=5.1.0.Final

最后我终于能做到这件事了

Specification<S> specification = (root, query, cb) -> {
  Join i18nJoin = root.join(collectionName, JoinType.LEFT);
  Join i18nDefaultJoin = root.join(collectionName, JoinType.LEFT);

  i18nJoin.on(cb.equal(i18nJoin.get("languageId"), 22));
  i18nDefaultJoin.on(cb.equal(i18nDefaultJoin.get("languageId"), 1));

  ... where clause and return ...
}

导致以下查询结果

SELECT *
FROM item i
LEFT JOIN i18n_item i18n ON i.item_id = i18n.item_id and i18n.language_id = 22
LEFT JOIN i18n_item i18n_default ON i.item_id = i18n_default.item_id i18n_default.language_id = 1

注意使用on方法不会覆盖关联注释(在本例中为@OneToMany)设置的原始子句,而是用您自己的条件扩展它。


1
我也想发同样的内容 :) (我现在才注意到这个问题)这被称为“交叉连接”(能够连接映射本身中不存在的其他字段)。而且,正如你已经发现的那样,它存在于Hibernate 5及以上版本中。 - Eugene
感谢您提供名称(cross-join):)。我已将其添加到答案中。这个问题困扰了我整整一周。我需要让它工作,以完成I18n实现,特别是在i18n字段上搜索。我很高兴这个功能现在成为Hibernate的一部分(我猜更多的人会在票证的评论中阅读)。 - Robin Hermans
很抱歉听到这个消息...如果我更快地看到了这个问题,我本可以更快地提供帮助 :) - Eugene

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接