Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
220 views
in Technique[技术] by (71.8m points)

java - Do I have to set both sides for a bidirectional relationship?

@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;
    }
}

To establish the relationship, I have to call

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

Why is both necessary, Why is it not sufficient to do only

b.setA(a);

or

a.getBs().add(b);

?

The relationship is stored in a join table, and b.setA(a) will update that join table.

But when I do a query afterwards, a.getBs() is empty. Why is that?

Here is a Test case that illustrates the question. Note that the very last assert fails.

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));

    }

}

Motivation: It can be useful to change a bidirectional relationship by changing "only one side", when the number of a.Bs gets large. In this case, an UPDATE SELECT query on the owning side is much faster than to call a.getBs().remove(b) See also here.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

There are 2 "facets" in the question: The Java side and the JPA side.

Java side

A more complete listing of the code might be:

@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;
    }
}

The JPA entites are still Java objects. If you do instruct Java explicitly to, e.g. "add the B in the collection of Bs, when its a property is set" it has no reason to do it automatically. Having said that, I have often seen patterns like (skipping null checking for brevity):

@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 side

JPA 2.1. specs, ch. 2.9 "Entity Relationships":

A bidirectional relationship has both an owning side and an inverse (non-owning) side. A unidirectional relationship has only an owning side. The owning side of a relationship determines the updates to the relationship in the database, as described in section 3.2.4.

  • The inverse side of a bidirectional relationship must refer to its owning side by use of the mappedBy element

In the setup of the question, B.a is the owning side because A.bs specifies mappedBy="a". The specifications says that the relation will be updated (i.e. an entry in the join table will be inserted) only when the owning side is updated. That is why doing b.setA(a) updates the join table.


After doing the above and successfully updating the DB, reading the related A object fresh from the DB should fetch the correct bs collection. To be sure, first try merging the B, committing the transaction, and fetching A (or refreshing it) in a different transaction. If you want the state of the Java objects to be reflected immediately in the same transaction, you have no other option but to set both b.a and a.getBs().add(b).


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

2.1m questions

2.1m answers

60 comments

57.0k users

...