为什么 Hibernate 不使用 @ManyToMany 和 @JoinTable 注释对这个 MANY TO MANY 关联 table 执行 JOIN?

Why Hibernate is not performing the JOIN on this MANY TO MANY association table using @ManyToMany and @JoinTable annotation?

我正在使用 Spring Data JPA 和 Hibernate 映射开发 Spring 启动应用程序,我遇到了以下问题。

我有这个 网络 table:

CREATE TABLE IF NOT EXISTS public.network
(
    id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
    name character varying(50) COLLATE pg_catalog."default" NOT NULL,
    description text COLLATE pg_catalog."default",
    CONSTRAINT network_pkey PRIMARY KEY (id)
)

和这个 table:

CREATE TABLE IF NOT EXISTS public.chain
(
    id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
    name character varying(50) COLLATE pg_catalog."default" NOT NULL,
    fk_chain_type bigint NOT NULL,
    description text COLLATE pg_catalog."default",
    CONSTRAINT chain_pkey PRIMARY KEY (id),
    CONSTRAINT "chain_to_chain_type_FK" FOREIGN KEY (fk_chain_type)
        REFERENCES public.chain_type (id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
        NOT VALID
)

这 2 个 table 通过 多对多 关系相互关联,由 network_chain[= 实现88=] table:

CREATE TABLE IF NOT EXISTS public.network_chain
(
    id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
    fk_network_id bigint NOT NULL,
    fk_chain_id bigint NOT NULL,
    CONSTRAINT network_chain_pkey PRIMARY KEY (id),
    CONSTRAINT chain_id_fk FOREIGN KEY (fk_chain_id)
        REFERENCES public.chain (id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
        NOT VALID,
    CONSTRAINT network_id_fk FOREIGN KEY (fk_network_id)
        REFERENCES public.network (id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
        NOT VALID
)

基本上 fk_network_id 字段表示进入 network table 的特定记录的 ID,而fk_chain_id表示进入的特定记录的id table.

我将这些数据库 table 映射到以下 Hibernate 实体 classes,首先我创建了这个 Network class 映射网络 table:

@Entity
@Table(name = "network")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Network implements Serializable {

    private static final long serialVersionUID = -5341425320975462596L;
    
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;
    
    @Column(name = "name")
    private String name;
    
    @Column(name = "description")
    private String description;
    
    @ManyToMany(cascade = { CascadeType.MERGE })
    @JoinTable(
        name = "network_chain", 
        joinColumns = { @JoinColumn(name = "fk_network_id") }, 
        inverseJoinColumns = { @JoinColumn(name = "fk_chain_id") }
    )
    Set<Chain> chainList;

}

如您所见,它包含 @ManyToMany 注释,使用 @JoinTable 注释来加入我的 network table 与我的 chain table(由 Chain 实体 class 映射) 使用前面的network_chain table(实现多对多关系)。

所以在这个 @JoinTable 注释中我指定:

  • 合并 table 实现多对多关系:network_chain.
  • 这个table上的两个FK是fk_network_idfk_chain_id.

然后我将这个 Spring 数据 JPA 存储库 class 命名为 NetworkRepository:

public interface NetworkRepository extends JpaRepository<Network, Integer> {
    
    /**
     * Retrieve a Network object by its ID
     * @param id of the network
     * @return the retrieve Network object
     */
    Network findById(String id);
    
    /**
     * Retrieve a Network object by its name
     * @param name of the network
     * @return a Network object
     */
    Network findByName(String name);
    
    /**
     * Retrieve the list of all the possible networks
     * @return a List<Network> object: the list of the all the networks
     */
    List<Network> findAll();

}

最后我创建了一个 JUnit 测试 class 包含一个方法来测试以前的存储库 findAll() 方法,这个:

@SpringBootTest()
@ContextConfiguration(classes = GetUserWsApplication.class)
@TestMethodOrder(OrderAnnotation.class)
public class NetworkTest {
    
    @Autowired
    private NetworkRepository networkRepository;
    
    /**
     * Retrieve the networks list
     */
    @Test
    @Order(1)
    public void findAllTest() {
        
        List<Network> networksList = this.networkRepository.findAll();
        
        assertTrue(networksList.size() == 5, "It retrieved 5 networks");
        
        Set<Chain> networkChain = networksList.get(0).getChainList();
        
        assertTrue(networkChain != null, "The network chains list are not null");
    }

}

问题是 findAll() 方法执行这个 SQL 语句(我可以在我的堆栈跟踪中看到它):

Hibernate: 
    select
        network0_.id as id1_6_,
        network0_.description as descript2_6_,
        network0_.name as name3_6_ 
    from
        network network0_

并检索预期的 List 对象,但正如您在以下打印屏幕中看到的,我的 chainList 字段给出错误:

它似乎没有从我的 MANY TO MANY table 中检索到 chainList(事实上,之前的 Hibernate 语句似乎没有对 network_chain 然后 chain tables).

我也尝试通过这一行直接访问这个字段(我的想法是,当显式执行对该字段的访问时,可能是 Hibernate 执行了这个连接:

Set<Chain> networkChain = networksList.get(0).getChainList();

当我尝试检索该字段时,似乎出现了 com.sun.jdi.InvocationException 异常。基本上似乎从未执行 MANY TO MANY table 和 chain table 之间的 JOINS。

为什么?我的代码有什么问题?我错过了什么?我该如何尝试修复它?

发生这种情况是因为您在 table (network_chain) 中有一个额外的列 (id),它负责 many-to-many 关系。

@JoinTable注解将只支持定义2个字段,通常是id中的2个table。 2 个 id 一起,将是 primary key 用于连接 table,也称为 composite primary key

在您的情况下,您必须创建一个实体来表示 table network_chain

@Entity
@Table(name = "network_chain")
public class NetworkChain {

   @Id
   @Column(name = "id")
   @GeneratedValue(strategy=GenerationType.IDENTITY)
   private Integer id;

   @ManyToOne
   private Network network;
   
   @ManyToOne
   private Chain chain;    
}

public class Network implements Serializable {
    //somewhere in the code
    @OneToMany
    List<NetworkChain> networkChains;
}

public class Chain implements Serializable {
    //somewhere in the code
    @OneToMany
    List<NetworkChain> networkChains;
}

现在,实体 NetworkChain 将额外的列 id 作为主键,hibernate 将进行适当的连接以获取数据。

题外话 但不确定为什么 findAll() 方法有一个 javadoc,它通常是 JpaRepository 和奇怪的 [=24] 的一部分=] 还有..