如何使用 JPQL、复合 Primary/Foreign 键和 MySQL 方言查找缺失的 @ManyToOne 关系
How to Find Missing @ManyToOne Relationships With JPQL, Compound Primary/Foreign Keys, and MySQL Dialect
我正在尝试做一些听起来很简单的事情,即查询员工存储库以获取根经理,其中根经理是没有经理的员工。有两件事使这个简单的问题复杂化:
- 底层员工table有复合主键
- 我需要引用“经理”员工的关系必须共享该主键的一个元素,非常类似于
鉴于我的单元测试通过了嵌入式 H2 数据库和相应的 H2 方言,我相信我的答案是正确的。但是,当我切换到 MySQL 或 MariaDB 方言进行生产时,我得到了似乎(至少对我而言)无效的 SQL,并且 MySQL 数据库抛出异常。
这是(简化的)员工 table,不幸的是,我无法更改其设计:
+-------------------------------+
| employees |
+------------+-------------+----+
| id | varchar(36) | PK |
| tenant_id | char(15) | PK |
| manager_id | varchar(36) | |
+------------+-------------+----+
这是实体:
@Entity
@Table(name = "employees")
@IdClass(EmployeeTenantKey.class)
public class Employee {
static final String ID_DEF = "varchar(36)";
static final String TID_DEF = "char(15) default 'default'";
@Id
@Column(name = "id", nullable = false, updatable = false, columnDefinition = ID_DEF)
private String id;
@Id
@Column(name = "tenant_id", nullable = false, updatable = false, columnDefinition = TID_DEF)
private String tenantId;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumnsOrFormulas(value = {
@JoinColumnOrFormula(formula = @JoinFormula(value = "tenant_id", referencedColumnName = "tenant_id")),
@JoinColumnOrFormula(column = @JoinColumn(name = "manager_id", referencedColumnName = "id",
columnDefinition = ID_DEF, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)))
})
private Employee manager;
...
}
最后,使用 JPQL 查找根管理器的 spring 数据存储库:
public interface EmployeeRepository extends JpaRepository<Employee, EmployeeTenantKey> {
@Query("select rootManager from Employee rootManager " +
"where not exists (" +
" select employee from Employee employee " +
" where rootManager.manager = employee" +
")")
List<Employee> findRootManagers();
}
当方言为org.hibernate.dialect.H2Dialect
时,这里是生成的SQL:
select * from employees employee0_
where not exists (
select (employee1_.id, employee1_.tenant_id) from employees employee1_
where employee0_.manager_id=employee1_.id and employee0_.tenant_id=employee1_.tenant_id
)
如果我将方言更改为 org.hibernate.dialect.MySQL8Dialect
或 org.hibernate.dialect.MariaDB103Dialect
,请注意第二个 where
子句中的更改:
select * from employees employee0_
where not exists (
select (employee1_.id, employee1_.tenant_id) from employees employee1_
where (employee0_.manager_id, employee0_.tenant_id)=(employee1_.id, employee1_.tenant_id)
)
虽然 H2 数据库引擎将在我的单元测试中接受此 SQL(尽管 MySQL 方言),但 MySQL 数据库引擎(通过 v8.0.21 Connector/J) 抛出以下错误:
java.sql.SQLException: Operand should contain 1 column(s)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129) ~[mysql-connector-java-8.0.21.jar:8.0.21]
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.21.jar:8.0.21]
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.21.jar:8.0.21]
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953) ~[mysql-connector-java-8.0.21.jar:8.0.21]
at com.mysql.cj.jdbc.ClientPreparedStatement.executeQuery(ClientPreparedStatement.java:1003) ~[mysql-connector-java-8.0.21.jar:8.0.21]
这是 Hibernate 方言中的错误吗?有没有更好的办法解决这个问题?
如果您想查看工作示例,我已在此处检查了简化的源代码:
https://github.com/chaserb/foreign-key-dereference
使用 Maven 3 克隆和构建后,您会看到生成的“select ...”SQL 紧邻构建中唯一的单元测试上方。走出大门,它使用的是 H2 方言。要更改此设置,请更新 src/main/resources/application-test.properties
然后重新 运行 Maven 构建。您会在生成的 SQL.
中看到差异
注意:我可以通过本机查询获得所需的结果。本机查询的逻辑对于问题的本质来说更直接,因为我能够直接处理外键元素——这是我在 JPQL 中不知道如何做的一个特性:
public interface EmployeeRepository extends JpaRepository<Employee, EmployeeTenantKey> {
@Query(value = "select * from employees emp " +
"where emp.manager_id is null or emp.manager_id = ''", nativeQuery = true)
List<Employee> findRootManagers();
}
但是,我尽量避免使用本机查询。
Is this a bug in the Hibernate dialects?
看起来确实是这样。例如,如果将内部查询的 select employee
替换为 select employee.id
,它是否有效?
Is there a better way to solve this problem?
好吧,您可以将 manager_id
的映射重复为一个简单的列:
@Column(name = "manager_id", insertable = false, updatable = false)
private String managerId;
然后您就可以在 JPQL 查询中使用 managerId
。
旁注 #1:最初我认为 SELECT e FROM Employee e WHERE e.manager IS NULL
是你问题的答案,但后来我测试了它,它被翻译成 where (employee.manager_id is null) AND (employee.tenant_id is null)
,这对 Hibernate 来说感觉相当愚蠢 - 在我看来应该使用 OR
来代替。
旁注 #2:您可能想看看 this approach to multitenancy,它可能使您能够使用更自然的映射方案。
我正在尝试做一些听起来很简单的事情,即查询员工存储库以获取根经理,其中根经理是没有经理的员工。有两件事使这个简单的问题复杂化:
- 底层员工table有复合主键
- 我需要引用“经理”员工的关系必须共享该主键的一个元素,非常类似于
鉴于我的单元测试通过了嵌入式 H2 数据库和相应的 H2 方言,我相信我的答案是正确的。但是,当我切换到 MySQL 或 MariaDB 方言进行生产时,我得到了似乎(至少对我而言)无效的 SQL,并且 MySQL 数据库抛出异常。
这是(简化的)员工 table,不幸的是,我无法更改其设计:
+-------------------------------+
| employees |
+------------+-------------+----+
| id | varchar(36) | PK |
| tenant_id | char(15) | PK |
| manager_id | varchar(36) | |
+------------+-------------+----+
这是实体:
@Entity
@Table(name = "employees")
@IdClass(EmployeeTenantKey.class)
public class Employee {
static final String ID_DEF = "varchar(36)";
static final String TID_DEF = "char(15) default 'default'";
@Id
@Column(name = "id", nullable = false, updatable = false, columnDefinition = ID_DEF)
private String id;
@Id
@Column(name = "tenant_id", nullable = false, updatable = false, columnDefinition = TID_DEF)
private String tenantId;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumnsOrFormulas(value = {
@JoinColumnOrFormula(formula = @JoinFormula(value = "tenant_id", referencedColumnName = "tenant_id")),
@JoinColumnOrFormula(column = @JoinColumn(name = "manager_id", referencedColumnName = "id",
columnDefinition = ID_DEF, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)))
})
private Employee manager;
...
}
最后,使用 JPQL 查找根管理器的 spring 数据存储库:
public interface EmployeeRepository extends JpaRepository<Employee, EmployeeTenantKey> {
@Query("select rootManager from Employee rootManager " +
"where not exists (" +
" select employee from Employee employee " +
" where rootManager.manager = employee" +
")")
List<Employee> findRootManagers();
}
当方言为org.hibernate.dialect.H2Dialect
时,这里是生成的SQL:
select * from employees employee0_
where not exists (
select (employee1_.id, employee1_.tenant_id) from employees employee1_
where employee0_.manager_id=employee1_.id and employee0_.tenant_id=employee1_.tenant_id
)
如果我将方言更改为 org.hibernate.dialect.MySQL8Dialect
或 org.hibernate.dialect.MariaDB103Dialect
,请注意第二个 where
子句中的更改:
select * from employees employee0_
where not exists (
select (employee1_.id, employee1_.tenant_id) from employees employee1_
where (employee0_.manager_id, employee0_.tenant_id)=(employee1_.id, employee1_.tenant_id)
)
虽然 H2 数据库引擎将在我的单元测试中接受此 SQL(尽管 MySQL 方言),但 MySQL 数据库引擎(通过 v8.0.21 Connector/J) 抛出以下错误:
java.sql.SQLException: Operand should contain 1 column(s)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129) ~[mysql-connector-java-8.0.21.jar:8.0.21]
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.21.jar:8.0.21]
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.21.jar:8.0.21]
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953) ~[mysql-connector-java-8.0.21.jar:8.0.21]
at com.mysql.cj.jdbc.ClientPreparedStatement.executeQuery(ClientPreparedStatement.java:1003) ~[mysql-connector-java-8.0.21.jar:8.0.21]
这是 Hibernate 方言中的错误吗?有没有更好的办法解决这个问题?
如果您想查看工作示例,我已在此处检查了简化的源代码:
https://github.com/chaserb/foreign-key-dereference
使用 Maven 3 克隆和构建后,您会看到生成的“select ...”SQL 紧邻构建中唯一的单元测试上方。走出大门,它使用的是 H2 方言。要更改此设置,请更新 src/main/resources/application-test.properties
然后重新 运行 Maven 构建。您会在生成的 SQL.
注意:我可以通过本机查询获得所需的结果。本机查询的逻辑对于问题的本质来说更直接,因为我能够直接处理外键元素——这是我在 JPQL 中不知道如何做的一个特性:
public interface EmployeeRepository extends JpaRepository<Employee, EmployeeTenantKey> {
@Query(value = "select * from employees emp " +
"where emp.manager_id is null or emp.manager_id = ''", nativeQuery = true)
List<Employee> findRootManagers();
}
但是,我尽量避免使用本机查询。
Is this a bug in the Hibernate dialects?
看起来确实是这样。例如,如果将内部查询的 select employee
替换为 select employee.id
,它是否有效?
Is there a better way to solve this problem?
好吧,您可以将 manager_id
的映射重复为一个简单的列:
@Column(name = "manager_id", insertable = false, updatable = false)
private String managerId;
然后您就可以在 JPQL 查询中使用 managerId
。
旁注 #1:最初我认为 SELECT e FROM Employee e WHERE e.manager IS NULL
是你问题的答案,但后来我测试了它,它被翻译成 where (employee.manager_id is null) AND (employee.tenant_id is null)
,这对 Hibernate 来说感觉相当愚蠢 - 在我看来应该使用 OR
来代替。
旁注 #2:您可能想看看 this approach to multitenancy,它可能使您能够使用更自然的映射方案。