【问题标题】:Do I have to set both sides for a bidirectional relationship?我是否必须为双向关系设置双方?
【发布时间】:2018-07-23 02:43:21
【问题描述】:
@Entity
public class A {
    @GeneratedValue
    @Id
    private long id;

    public long getId() {
        return id;
    }

    public void setId(final long id) {
        this.id = id;
    }

    @OneToMany(mappedBy = "a")
    List<B> bs;

    public List<B> getBs() {
        return bs;
    }

    public void setBs(final List<B> bs) {
        this.bs = bs;
    }
}
@Entity
public class B {
    @GeneratedValue
    @Id
    private long id;

    public long getId() {
        return id;
    }

    public void setId(final long id) {
        this.id = id;
    }

    @ManyToOne
    @JoinTable
    A a;

    public A getA() {
        return a;
    }

    public void setA(final A a) {
        this.a = a;
    }
}

要建立关系,我必须打电话

b.setA(a);
a.getBs().add(b);

为什么两者都是必要的,为什么只做是不够的

b.setA(a);

a.getBs().add(b);

?

关系存储在连接表中,b.setA(a) 将更新该连接表。

但是当我之后进行查询时,a.getBs() 是空的。这是为什么呢?

这是一个说明问题的测试用例。请注意,最后一个断言失败。

public class QuickTestAB2 {

    private static String dbUrlBase = "jdbc:derby:testData/db/test.db";

    private static String dbUrlCreate = dbUrlBase + ";create=true";

    private static String dbUrlDrop = dbUrlBase + ";drop=true";

    private EntityManagerFactory factory;

    private EntityManager em;

    public Map<String, String> createPersistenceMap(final String dbUrl) {
        final Map<String, String> persistenceMap = new HashMap<>();
        persistenceMap.put("javax.persistence.jdbc.url", dbUrl);
        return persistenceMap;
    }

    public void dropDatabase() throws Exception {
        if (em != null && em.isOpen()) {
            em.close();
        }
        if (factory != null && factory.isOpen()) {
            factory.close();
        }
        try (Connection conn = DriverManager.getConnection(dbUrlDrop)) {

        } catch (final SQLException e) {
            // always

        }
    }

    public void deleteDatabase() throws Exception {
        dropDatabase();
        final File file = new File("testData/db/test.db");
        if (file.exists()) {
            FileUtils.forceDelete(file);
        }
    }

    public void createNewDatabase() throws SQLException, IOException {

        FileUtils.forceMkdir(new File("testData/db"));
        try (Connection conn = DriverManager.getConnection(dbUrlCreate)) {

        }
    }

    @BeforeClass
    public static void setUpBeforeClass01() throws Exception {
        Tests.enableLog4J();
        JPATests.enableJPA();

    }

    @AfterClass
    public static void tearDownAfterClass01() throws Exception {

    }

    @Before
    public void setUp01() throws Exception {

        deleteDatabase();
        createNewDatabase();
        final Map<String, String> map = createPersistenceMap(dbUrlCreate);
        factory = Persistence.createEntityManagerFactory("pu", map);

    }

    @After
    public void tearDown01() throws Exception {
        if (em != null && em.isOpen()) {
            em.close();
        }
        em = null;
        if (factory != null && factory.isOpen()) {
            factory.close();
        }
        factory = null;
    }

    @Test
    public void test01() throws Exception {
        em = factory.createEntityManager();
        final A a = new A();
        final B b = new B();
        b.setA(a);
        try {
            em.getTransaction().begin();
            em.persist(a);
            em.persist(b);
            em.getTransaction().commit();
        } finally {
            em.close();
        }
        em = factory.createEntityManager();
        B b2;
        A a2;
        try {
            em.getTransaction().begin();
            Query q = em.createQuery("SELECT b FROM B b");
            b2 = (B) q.getSingleResult();
            q = em.createQuery("SELECT a FROM A a");
            a2 = (A) q.getSingleResult();
            em.getTransaction().commit();
        } finally {
            em.close();
        }
        assertThat(a2, is(not(nullValue())));
        assertThat(b2, is(not(nullValue())));

        assertThat(b2.getA(), is(not(nullValue())));
        assertThat(a2.getBs().isEmpty(), is(false));

    }

}

动机:a.Bs 的数量变大时,通过更改“仅一侧”来更改双向关系会很有用。在这种情况下,拥有方的UPDATE SELECT 查询比调用a.getBs().remove(b) 快得多 另见here

【问题讨论】:

    标签: java jpa eclipselink derby


    【解决方案1】:

    问题中有 2 个“方面”:Java 端和 JPA 端。

    Java 端

    更完整的代码清单可能是:

    @Entity
    class A {
        @OneToMany(mappedBy = "a")
        @JoinTable
        List<B> bs;
    
        public List<B> getBs() {
            return bs;
        }
        public void setBs(List<B> bs) {
            this.bs = bs;
        }
    }
    
    @Entity
    class B {
        @ManyToOne
        @JoinTable
        A a;
    
        public A getA() {
            return a;
        }
        public void setA(A a) {
            this.a = a;
        }
    }
    

    JPA 实体仍然是 Java 对象。如果确实明确指示Java,例如“在 Bs 集合中添加 B,当其 a 属性设置时”它没有理由自动执行此操作。话虽如此,我经常看到类似的模式(为了简洁而跳过空值检查):

    @Entity
    class A {
        ...
    
        public void addB(B b) {
            bs.add(b);
            b.setA(this);
        }
        public void removeB(B b) {
            if( bs.remove(b) ) {
                b.setA(null);
            }
        }
    }
    

    JPA 方面

    JPA 2.1。规格,通道2.9 “实体关系”:

    双向关系既有拥有方,也有反向(非拥有)方。单向关系只有拥有方。关系的拥有方决定数据库中关系的更新,如第 3.2.4 节所述。

    • 双向关系的反面必须使用 mappedBy 元素引用其拥有方

    在问题的设置中,B.a 是拥有方,因为A.bs 指定了mappedBy="a"。规范说,只有当 owning 方更新时,才会更新关系(即插入连接表中的条目)。这就是为什么 b.setA(a) 会更新连接表。


    完成上述并成功更新数据库后,从数据库中读取相关的 A 对象应该会获取正确的 bs 集合。可以肯定的是,首先尝试合并 B,提交事务,然后在不同的事务中获取 A(或刷新它)。如果您希望 Java 对象 的状态立即反映在同一事务中,您别无选择,只能同时设置 b.aa.getBs().add(b)

    【讨论】:

    • 非常感谢您的出色回答!你描述了我的期望。然而,我很难证实这一点。请参阅更新问题中的测试用例。
    • 嗨,谢谢。我不是 100% 确定,但我敢打赌,您通过在关系的非拥有方指定 @JoinTable 来混淆 JPA,即 A.bs(请参阅 JPA 2.2 规范第 11.1.27 章)。如果您查看在测试代码的 Derby 实例中创建的表,您可以验证这一点——应该有 2 个连接表。不管怎样,你能把@JoinTableA.bs中删除,再试一次吗?
    • 检查过,只有一张桌子。我还从A.bs中删除了可连接注释,仍然没有成功。
    【解决方案2】:

    Nikos Paraskevopoulos' answer below 中所述,JPA 和 java 对象要求您设置关系的双方。只要你设置了拥有方,数据库就会随着关系的变化而更新,但非拥有方只有在你手动设置,或者你强制刷新或重新加载时才会反映数据库中的内容。从单独的上下文中读取实体不会强制重新加载,因为您的 JPA 提供程序可以使用二级缓存;这是 EclipseLink 中的 default。您的另一个读取是从共享缓存中返回 A,它与您的原始对象一样,没有将 B 添加到其 B 列表中。

    最简单的解决方案是预先将 B 设置到 A 的列表中。不过,这里的其他选项是使用 em.refresh(a) 或使用查询提示强制刷新 A,或者禁用共享缓存。

    【讨论】:

      猜你喜欢
      • 2022-06-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-12-05
      • 2014-07-23
      • 2012-08-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多