50

QQA: Hibernate 为什么需要手工管理双向关联

 5 years ago
source link: http://lotabout.me/2018/QQA-Why-should-bidirectional-associations-be-managed-manually/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Hibernate/JPA 中如果两个 Entity 之间的关联是双向的(不论是 @ManyToMany@OneToMany 还是 @OneToOne ),都需要手动管理关联,为什么?

  • 调用 entityManager.persist 保存对象时 Hibernate/JPA 不会直接执行 SQL,而会等到 entityManager.flush 或事务 commit 时完成。
  • 同理 entityManager.load 也可能只会从内存中获取对象(可以认为是某种缓存)。
  • 如果不手动管理双向关联,则从内存获取的对象并不会反映数据库中的映射关系。

双向关联的本质是告诉 Hibernate 让两个实体共用一张数据库表(或表结构)。

这里以 @ManyToMany 为例(参考 Hibernate User Guide ) :有两个实体 PersonAddress ,一个 Person 可以拥有多个 Address,而一个 Address 也可以属于多个 Person。于是设计实体如下:

@Entity
public static class Person {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToMany
    private List<Address> addresses = new ArrayList<>();

    // ... omit all other stuff
}

@Entity
public static class Address {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToMany
    private List<Person> owners = new ArrayList<>();

    // ... omit all other stuff
}

问题来了,我们应该创建一张关联表还是两张呢?其实取决于使用业务含义。即如果 Personaddresses 的含义是“人的居住地址”,而 Address 中的 owners 与之对应,表达的是“地址上居住的人”,则它们应该是一张关联表。但如果 Addressowners 表达的是“地址的主人(如房东)”,则二者就不应该共用一张关联表。

如何告诉 Hibernate 需要共用一张表呢?通过 mappedBy

@Entity
public static class Person {
    @ManyToMany
    private List<Address> addresses = new ArrayList<>();
    // ... omit all other methods
}

@Entity
public static class Address {
    @ManyToMany(mappedBy = "addresses")
    private List<Person> owners = new ArrayList<>();

    // ... omit all other methods
}

(mappedBy = "addresses") 的含义是这个字段与 Person 中的 addresses 字段共用表结构。

这里最后重点是双向关系一定是从属关系,有一方是 owner,另一方是 follower(标记了 mappedBy 的一方)。只有在 owner 这方添加关联并保存时,Hibernate 才会存入关联表,反之不会。例如我们只能通过 person.addAddress() 并保存 person 的方式来完成添加关联而不能用 address.addPerson() 后保存 address 的方式。

手工管理关联是什么意思

例如我们在实现 Person.addAddress 时,需要这样实现:

@Entity
public static class Person {
    //...omit other fields

    @ManyToMany
    private List<Address> addresses = new ArrayList<>();

    public void addAddress(Address address) {
        addresses.add( address );
        address.getOwners().add( this );
    }

    public void removeAddress(Address address) {
        addresses.remove( address );
        address.getOwners().remove( this );
    }
    // ... omit all other methods
}

即在为 person 添加 address 时,我们需要将当前的 person 添加到 address的 owners 字段中;删除时相似。“管理关联”表示需要在代码级别来管理关联双方实体的联系。

如果从数据库的角度思考,我们知道 PersonAddress 的关系是存储在一张关联表里的,一个关联存入这张表后,不论哪一方读取,都应该反映出新的关联关系,而在 Hibernate 这一层,却需要我们显式地(从另一方的 set )中添加/删除这个关联,显得不可思议。

另外,注意我们往 set 中添加 addressperson 时,需要我们正确的实现 PersonAddressequalshashCode 方法,这是另一个坑,这里就不深入了。

为什么需要手工管理

终于到了“为什么”部分了,首先是如果不手工管理会发生什么。考虑下面的测试:

@Test
@Transactional
public void test() {
    Person person = repository.findPersonById(1);
    Address address = repository.findAddressById(20);
    person.getAddresses.add(address);
    repository.save(person);

    System.out.println(address.getOwners().size()) // what is the result?

    Address address = repository.findAddressById(20);
    System.out.println(address.getOwners().size()) // what is the result?
}

答案是两个 size 都为 0

save
find

注意上面说的是一般的情况,什么时候执行 SQL 取决于具体的配置,一般会在事务前的 commit

因此,如果在 save 之后还需要使用到 address ,就不要期待它会立即反映出数据库中的修改;反之,如果 save 之后就不再使用到 address ,那即使不手工管理(同步) 关联关系也不会有多大影响。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK