如何使用 JPQL、复合 Primary/Foreign 键和 MySQL 方言查找缺失的 @ManyToOne 关系

How to Find Missing @ManyToOne Relationships With JPQL, Compound Primary/Foreign Keys, and MySQL Dialect

我正在尝试做一些听起来很简单的事情,即查询员工存储库以获取根经理,其中根经理是没有经理的员工。有两件事使这个简单的问题复杂化:

  1. 底层员工table有复合主键
  2. 我需要引用“经理”员工的关系必须共享该主键的一个元素,非常类似于

鉴于我的单元测试通过了嵌入式 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.MySQL8Dialectorg.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,它可能使您能够使用更自然的映射方案。