Học viên: Phạm Thế Dương
Email: duongphamhn97@gmail.com
Bài viết gốc: https://thorben-janssen.com/ultimate-guide-association-mappings-jpa-hibernate

Ánh xạ quan hệ là một trong những tính năng cốt lõi của JPA và Hibernate. Chúng mô hình hoá mối quan hệ giữa hai bảng trong cơ sở dữ liệu dưới dạng thuộc tính trong model. Điều này cho phép bạn dễ dàng truy cập tới các liên kết trong model và JPQL hoặc Criteria queries.

JPA và Hibernate hỗ trợ các quan hệ giống như trong mô hình cơ sở dữ liệu quan hệ. Bạn có thể sử dụng:

  • Quan hệ one-to-one (một-một),
  • Quan hệ many-to-one (nhiều-một)
  • Quan hệ many-to-many (nhiều-nhiều).

Bạn có thể ánh xạ mỗi quan hệ này dưới dạng unidirectional (một chiều) hoặc bidirectional (hai chiều). Nghĩa là bạn có thể mô hình hoá chúng dưới dạng thuộc tính của một entity hoặc cả hai entity. Điều này không ảnh hưởng tới ánh xạ cơ sở dữ liệu, nhưng nó xác định chiều mà bạn có thể sử dụng mối quan hệ trong model và JPQL hoặc Criteria queries. Tôi sẽ giải thích rõ hơn ở ví dụ đầu tiên.

Quan hệ many-to-one

Một đơn hàng bao gồm nhiều danh mục, nhưng mỗi danh mục chỉ thuộc về một đơn hàng. Đó là ví dụ điển hình về quan hệ many-to-one. Nếu bạn muốn mô hình hoá quan hệ này trong cơ sở dữ liệu, bạn cần lưu khoá chính của bản ghi Order dưới dạng khoá ngoại trong bảng OrderItem.

Với JPA và Hibernate, bạn có thể mô hình hoá điều này với 3 cách. Bạn có thể mô hình hoá nó dưới dạng quan hệ bidirectional với thuộc tính trong Order entity và OrderItem entity. Hoặc bạn có thể mô hình hoá nó dưới dạng quan hệ unidirectional với một thuộc tính trong Order entity hoặc trong OrderItem entity

Quan hệ unidirectional many-to-one

Hãy xem ánh xạ unidirectional trên OrderItem entity trước. OrderItem entity đại diện cho phía many của quan hệ và bảng OrderItem chứa khoá ngoại của bản ghi trong bảng Oder.

Như có thể thấy ở đoạn code dưới đây, bạn có thể mô hình hoá quan hệ này với một thuộc tính có kiểu dữ liệu Order và một @ManyToOne annotation. Thuộc tính Order order mô hình hoá quan hệ này, và annotation yêu cầu Hibernate ánh xạ nó vào cơ sở dữ liệu.

@Entity
public class OrderItem {
 
    @ManyToOne
    private Order order;
 
    …
}

Đó là tất cả những gì bạn cần để mô hình hoá quan hệ này. Mặc định Hibernate sẽ tự sinh tên của cột khoá ngoại dựa trên tên của thuộc tính ánh xạ quan hệ và tên của thuộc tính khoá chính. Ở ví dụ này, Hibernate sẽ sử dụng một cột với tên order_id để lưu khoá ngoại tới Order entity.

Nếu bạn muốn sử dụng một cột khác, bạn cần phải định nghĩa cột khoá ngoại với @JoinColumn annotation. Ví dụ đoạn code dưới đây yêu cầu Hibernate sử dụng cột fk_order để lưu khoá ngoại.

@Entity
public class OrderItem {
 
    @ManyToOne
    @JoinColumn(name = “fk_order”)
    private Order order;
 
    …
}

Giờ bạn có thể sử dụng quan hệ này để lấy Order của một OrderItem và thêm vào hoặc xoá một OrderItem khỏi một Order.

Order o = em.find(Order.class, 1L);
 
OrderItem i = new OrderItem();
i.setOrder(o);
 
em.persist(i);

Hiện giờ đó là tất cả về ánh xạ quan hệ unidirectional many-to-one. Nếu bạn muốn đi sâu hơn, hãy tìm hiểu về FetchTypes. Tôi đã giải thích chúng một cách chi tiết trong bài Introduction to JPA FetchTypes.

Tuy nhiên hãy tiếp tục với ánh xạ quan hệ và quan hệ unidirectional one-to-many, nó cũng rất giống với ánh xạ quan hệ unidirectional many-to-one.

Quan hệ unidirectional one-to-many

Ánh xạ quan hệ unidirectional one-to-many không được sử dụng phổ biến. Ví dụ này chỉ mô hình hoá quan hệ trên Order entity mà không phải OrderItem entity.
Định nghĩa ánh xạ cơ bản rất giống với quan hệ many-to-one. Nó gồm có thuộc tính List items chứa các entity liên kết và một @OneToMany annotation.

@Entity
public class Order {
 
    @OneToMany
    private List<OrderItem> items = new ArrayList<OrderItem>();
 
    …
}

Nhưng đây dường như không phải cách ánh xạ bạn mong muốn vì Hibernate sử dụng một bảng liên kết để ánh xạ quan hệ. Nếu bạn muốn tránh điều này, bạn cần sử dụng @JoinColumn annotation để xác định cột khoá ngoại.

Đoạn code dưới đây biểu diễn việc ánh xạ này. @JoinColumn annotation yêu cầu Hibernate sử dụng cột fk_order trong bảng OrderItem để liên kết hai bảng.

@Entity
public class Order {
 
    @OneToMany
    @JoinColumn(name = “fk_order”)
    private List<OrderItem> items = new ArrayList<OrderItem>();
 
    …
}

Giờ bạn có thể sử dụng quan hệ này để lấy tất cả OrderItems của một Order và thêm vào hoặc xoá một OrderItem khỏi một Order.

Order o = em.find(Order.class, 1L);
 
OrderItem i = new OrderItem();
 
o.getItems().add(i);
 
em.persist(i);

Quan hệ bidirectional many-to-one

Việc ánh xạ quan hệ bidirectional many-to-one là cách thông thường nhất để mô hình hoá quan hệ này với JPA và Hibernate. Nó sử dụng một thuộc tính trong Order entity và OrderItem entity. Nó cho phép bạn điều hướng liên kết theo cả hai chiều trong model và JPQL queries.

Hãy xem bên hữu trước. Bạn đã biết việc ánh xạ này với việc ánh xạ quan hệ unidirectional many-to-one. Nó chứa thuộc tính Order order, một @ManyToOne annotation mà một @JoinColumn annotation tuỳ ý.

@Entity
public class OrderItem {
 
    @ManyToOne
    @JoinColumn(name = “fk_order”)
    private Order order;
 
    …
}

Bên hữu của ánh xạ quan hệ đã cung cấp tất cả thông tin cần thiết để Hibernate ánh xạ nó vào cơ sở dữ liệu. Điều này khiến việc định nghĩa bên tham chiếu trở nên đơn giản. Bạn chỉ cần tham chiếu ánh xạ quan hệ sở hữu. Bạn có thể làm thế bằng cách cung cấp tên của thuộc tính ánh xạ quan hệ vào thuộc tính mappedBy của @OneToMany annotation. Ở ví dụ này, đó là thuộc tính order của OrderItem entity.

@Entity
public class Order {
 
    @OneToMany(mappedBy = “order”)
    private List<OrderItem> items = new ArrayList<OrderItem>();
 
    …
}

Giờ bạn có thể sử dụng quan hệ này theo cách giống như quan hệ unidirectional tôi đã thể hiện ở trên. Nhưng thêm vào và xoá một entity khỏi quan hệ yêu cầu thêm một bước. Bạn cần cập nhật ở cả hai phía của quan hệ.

Order o = em.find(Order.class, 1L);
 
OrderItem i = new OrderItem();
i.setOrder(o);
 
o.getItems().add(i);
 
em.persist(i);

Đó là một việc dễ phát sinh lỗi, và nhiều lập trình viên thích triển khai một phương thức utility để cập nhật cả hai entity.

@Entity
public class Order {
    …
         
    public void addItem(OrderItem item) {
        this.items.add(item);
        item.setOrder(this);
    }
    …
}

Hiện giờ đó là tất cả về ánh xạ quan hệ many-to-one. Bạn có thể tìm hiểu @FetchTypes và cách chúng ảnh hưởng tới việc Hibernate nạp các entity từ cơ sở dữ liệu. Tôi giải thích chi tiết về chúng trong Introduction to JPA FetchTypes.

Quan hệ many-to-many

Quan hệ many-to-many cũng là một trường hợp hay gặp. Ở cấp độ cơ sở dữ liệu, nó yêu cầu một bảng liên kết chứa cặp khoá chính của cả hai entity. Nhưng bạn sẽ không cần phải ánh xạ bảng này vào một entity.

Một ví dụ điển hình của quan hệ many-to-many là sản phẩm và cửa hàng. Mỗi cửa hàng bán nhiều sản phẩm và mỗi sản phẩm được bán ở nhiều cửa hàng.

Tương tự với quan hệ many-to-one, bạn có thể mô hình hoá quan hệ many-to-many với quan hệ unidirectional hay bidirectional giữa hai entity.

Nhưng có một khác biệt quan trọng bạn có thể chưa nhận ra trong đoạn code dưới đây. Khi bạn ánh xạ quan hệ many-to-many, bạn nên sử dụng Set thay vì List cho thuộc tính. Nếu không, Hibernate sẽ thực hiện một thao tác không hiệu quả để xoá entity khỏi quan hệ. Nó sẽ xoá toàn bộ bản ghi khỏi bảng liên kết và lại thêm vào những bản ghi còn lại. Bạn có thể tránh điều này bằng cách sử dụng Set thay vì List.

Ok, hãy xem ánh xạ unidirectional trước.

Quan hệ unidirectional many-to-many

Tương tự với ánh xạ trước, quan hệ unidirectional many-to-many yêu cầu một thuộc tính và một @ManyToMany annotation. Thuộc tính mô hình hoá quan hệ và bạn có thể sử dụng nó để điều hướng trong model hoặc JPQL queries. Annotation yêu cầu Hibernate ánh xạ quan hệ many-to-many.

Hãy xem ánh xạ quan hệ giữa Store (cửa hàng) và Product (sản phẩm). Thuộc tính Set products mô hình hoá quan hệ trong model và @ManyToMany annotation yêu cầu Hibernate ánh xạ nó dưới dạng quan hệ many-to-many.

Và như tôi đã giải thích, hãy chú ý sự khác biệt với ánh xạ many-to-one trước đó. Bạn nên ánh xạ entity liên kết tới một Set thay vì List.

@Entity
public class Store {
 
    @ManyToMany
    private Set<Product> products = new HashSet<Product>();
 
    …
}

Nếu bạn không cung cấp thêm thông tin gì, Hibernate sẽ sử dụng ánh xạ mặc định với một bảng liên kết với tên của hai entity và các thuộc tính khoá chính của hai entity. Trong trường hợp này, Hibernate sử dụng bảng Store_Product với các cột store_idproduct_id.

Bạn có thể tuỳ biến điều này với @JoinTable annotation cùng các thuộc tính của nó là joinColumnsinverseJoinColumns. Thuộc tính joinColumns xác định các cột khoá ngoại cho entity mà bạn định nghĩa ánh xạ quan hệ. Thuộc tính inverseJoinColumns xác định các cột khoá ngoại của entity liên kết.

Đoạn code sau thể hiện ánh xạ yêu cầu Hibernate sử dụng bảng store_product với cột fk_product là khoá ngoại tới bảng Product và cột fk_store là khoá ngoại tới bảng Store.

@Entity
public class Store {
 
    @ManyToMany
    @JoinTable(name = “store_product”,
           joinColumns = { @JoinColumn(name = “fk_store”) },
           inverseJoinColumns = { @JoinColumn(name = “fk_product”) })
    private Set<Product> products = new HashSet<Product>();
 
    …
}

Đó là toàn bộ những gì bạn cần làm để định nghĩa quan hệ unidirectional many-to-many giữa hai entity. Giờ bạn có thể sử dụng một Set các entity liên kết trong model hoặc liên kết các bảng ánh xạ trong JPQL query.

Store s = em.find(Store.class, 1L);
 
Product p = new Product();
 
s.getProducts().add(p);
 
em.persist(p);

Quan hệ bidirectional many-to-many

Ánh xạ quan hệ bidirectional cho phép bạn điều hướng quan hệ theo cả hai chiều. Nếu bạn đã đọc đến đây, bạn sẽ không ngạc nhiên khi tôi nói ánh xạ này tuân theo ý tưởng giống với ánh xạ quan hệ bidirectional many-to-one.

Một trong hai entity sở hữu quan hệ và cung cấp tất cả các thông tin ánh xạ. Entity còn lại chỉ tham chiếu tới ánh xạ quan hệ để cho Hibernate biết nơi nó có thể lấy thông tin cần thiết.

Hãy bắt đầu với entity sở hữu quan hệ. Ánh xạ ở đây giống với ánh xạ quan hệ unidirectional many-to-many. Bạn cần một thuộc tính ánh xạ quan hệ trong model và một @ManyToMany annotation. Nếu bạn muốn thay đổi ánh xạ mặc định, bạn có thể dùng @JoinColumn annotation.

@Entity
public class Store {
 
    @ManyToMany
    @JoinTable(name = “store_product”,
           joinColumns = { @JoinColumn(name = “fk_store”) },
           inverseJoinColumns = { @JoinColumn(name = “fk_product”) })
    private Set<Product> products = new HashSet<Product>();
 
    …
}

Việc ánh xạ ở bên tham chiếu thì dễ dàng hơn nhiều. Tương tự với ánh xạ quan hệ bidirectional many-to-one, bạn chỉ cần tham chiếu tới thuộc tính sở hữu quan hệ.

Bạn có thể xem ví dụ dưới đây. Thuộc tính Set<Product> products của Store entity sở hữu quan hệ. Vì thế, bạn chỉ cần cung cấp Stringproducts” cho thuộc tính mappedBy của @ManyToMany annotation.

@Entity
public class Product{
 
    @ManyToMany(mappedBy=”products”)
    private Set<Store> stores = new HashSet<Store>();
 
    …
}

Đó là tất cả những gì bạn cần để định nghĩa quan hệ bidirectional many-to-many giữa hai entity. Nhưng có một việc bạn có thể làm khiến cho việc sử dụng quan hệ bidirectional dễ dàng hơn.

Bạn phải cập nhật ở cả hai chiều của quan hệ bidirectional khi bạn muốn thêm hoặc xoá entity. Làm thế thì dài dòng và dễ gây lỗi. Vì thế một cách tốt là tạo các phương thức helper để cập nhật hai entity liên kết.

@Entity
public class Store {
 
    public void addProduct(Product p) {
        this.products.add(p);
        p.getStores().add(this);
    }
 
    public void removeProduct(Product p) {
        this.products.remove(p);
        p.getStores().remove(this);
    }
 
    …
}

Ok, giờ ta đã xong với ánh xạ quan hệ many-to-many. Hãy xem quan hệ thứ ba và cũng là cuối cùng: quan hệ one-to-one.

Quan hệ one-to-one

Quan hệ one-to-one hiếm khi được sử dụng trong các mô hình bảng quan hệ. Vì thế bạn không cần ánh xạ này quá nhiều. Nhưng rồi bạn sẽ gặp nó theo thời gian. Cho nên sẽ tốt khi biết rằng bạn có thể ánh xạ nó theo cách giống với các quan hệ khác.

Một ví dụ cho quan hệ one-to-one là khách hàng và địa chỉ giao hàng. Mỗi khách hàng có chính xác một địa chỉ giao hàng và mỗi địa chỉ giao hàng thuộc về một khách hàng. Ở cấp độ cơ sở dữ liệu, quan hệ này được ánh xạ với một cột khoá ngoại trong bảng ShippingAddress (địa chỉ giao hàng) hoặc bảng Customer (khách hàng).

Hãy xem ánh xạ unidirectional trước.

Quan hệ unidirectional one-to-one

Giống như trong ánh xạ unidirectional trước, bạn chỉ cần mô hình hoá nó vào entity để có thể điều hướng quan hệ trong query hoặc model. Ví dụ bạn muốn lấy Customer từ ShippingAddress entity.

Ánh xạ này tương tự với các ánh xạ đã nói ở trên. Bạn cần một thuộc tính của entity đại diện cho quan hệ, và bạn cần đánh dấu nó với một @OneToOne annotation.

Khi đó, Hibernate sẽ sử dụng tên của entity liên kết và tên khoá chính của nó để sinh ra tên của cột khoá ngoại. Ở ví dụ này, nó sẽ sử dụng cột shippingaddress_id. Bạn có thể tuỳ biến tên của cột khoá ngoại với @JoinColumn annotation. Đoạn code sau biểu diễn ánh xạ này.

@Entity
public class Customer{
 
    @OneToOne
    @JoinColumn(name = “fk_shippingaddress”)
    private ShippingAddress shippingAddress;
 
    …
}

Đó là tất cả những gì bạn cần để định nghĩa một ánh xạ quan hệ one-to-one. Giờ bạn có thể sử dụng nó trong nghiệp vụ của mình để thêm hoặc xoá một quan hệ, và điều hướng nó trong model hoặc liên kết nó trong JPQL query.

Customer c = em.find(Customer.class, 1L);
ShippingAddress sa = c.getShippingAddress();

Quan hệ bidirectional one-to-one

Ánh xạ quan hệ bidirectional one-to-one mở rộng ánh xạ unidirectional để bạn có thể điều hướng theo cả hai chiều. Ở ví dụ này, bạn có thể mô hình hoá nó trong ShippingAddress entity để bạn có thể lấy Customer của một ShippingAddress.

Tương tự với các ánh xạ bidirectional đã nói ở trên, ánh xạ bidirectional one-to-one bao gồm một bên sở hữu và một bên tham chiếu. Bên sở hữu của quan hệ định nghĩa ánh xạ, và bên tham chiếu chỉ liên kết tới ánh xạ đó.

Đinh nghĩa bên sỡ hữu của ánh xạ giống như trong ánh xạ unidirectional. Nó gồm một thuộc tính mô hình hoá quan hệ và được đánh dấu bởi một @OneToOne annotation và một @JoinColumn annotation tuỳ ý.

@Entity
public class Customer{
 
    @OneToOne
    @JoinColumn(name = “fk_shippingaddress”)
    private ShippingAddress shippingAddress;
 
    …
}

Bên tham chiếu của quan hệ chỉ liên kết tới thuộc tính sở hữu quan hệ. Hibernate lấy tất cả thông tin từ ánh xạ được tham chiếu, và bạn không cần phải cung cấp thông tin gì thêm. Bạn có thể định nghĩa điều này với thuộc tính mappedBy của @OneToOne annotation. Đoạn code sau thể hiện ánh xạ này.

@Entity
public class ShippingAddress{
 
    @OneToOne(mappedBy = “shippingAddress”)
    private Customer customer;
 
    …
}

Tổng kết

Mô hình bảng quan hệ sử dụng các quan hệ many-to-many, many-to-one và one-to-one để mô hình hoá quan hệ giữa các bản ghi trong cơ sở dữ liệu. Bạn có thể ánh xạ các quan hệ tương tự với JPA và Hibernate, và bạn có thể dùng unidirectional hay bidirectional.

Ánh xạ unidirectional định nghĩa quan hệ trong 1 trên 2 entity liên kết, và bạn chỉ có thể điều hướng theo chiều đó. Ánh xạ bidirectional mô hình hoá quan hệ cho cả hai entity để bạn có thể điều hướng theo cả hai chiều.

Khái niệm ánh xạ của cả 3 loại quan hệ là giống nhau.

Nếu bạn muốn tạo một ánh xạ unidirectional, bạn cần một thuộc tính trong entity mà mô hình hoá quan hệ và được đánh dấu bởi @ManyToMany, @ManyToOne, @OneToMany hoặc @OneToOne annotation. Hibernate sẽ sinh tên của các cột khoá ngoại cần thiết và các bảng dựa trên tên của các entity và thuộc tính khoá chính của chúng.

Quan hệ bidirectional gồm một bên sở hữu và một bên tham chiếu. Bên sở hữu của quan hệ giống như trong ánh xạ unidirectional và định nghĩa ánh xạ. Bên tham chiếu chỉ liên kết tới thuộc tính sở hữu quan hệ.