Written By : Nguyễn Văn Linh
Gmail : Linhthai130100@gmail.com

1. Giới Thiệu

Hướng dẫn Hibernate nhanh này sẽ đưa chúng ta qua một ví dụ về ánh xạ một - nhiều bằng cách sử dụng JPA annotation ( chú thích ) . Đây là một giải pháp thay thế cho XML

Bài viết tham khảo : https://www.baeldung.com/hibernate-one-to-many

2. Mô tả

Nói một cách đơn giản, ánh xạ một-nhiều có nghĩa là một hàng trong bảng này được ánh xạ tới nhiều hàng trong bảng khác.

Dưới đây là hình ảnh ví dụ về mối liên kết một-nhiều của 2 thực thể

Đối với ví dụ này , chúng tối sẽ triển khai hệ thống giỏ hàng trong đó chúng tôi có 1 bảng cho mỗi giỏ hàng (cart) và một bảng khác cho mỗi mặt hàng (items) . Mỗi giỏ hàng có thể có nhiều mặt hàng , vì vậy ở đây chúng tôi có một ánh xạ một đến nhiều

Cách thức hoạt động ở cấp dữ liệu là chúng ta có cart_id làm khóa chính (primary key) trong bảng giỏ hàng và cũng có cart_id làm khóa ngoại (foreign key) trong các mặt hàng

Chúng tôi sử dụng annotation @OneToMany

Hãy ánh xạ lớp Cart với tập hợp các đối tượng Item theo cách phản ánh mối quan hệ trong cơ sở dữ liệu:

public class Cart {

    //...     
 
    @OneToMany(mappedBy="cart")
    private Set<Item> items;
	
    //...
}

Chúng ta cũng có thể tham chiếu đến Giỏ hàng của mỗi Mặt hàng bằng annotation @ManyToOne , làm cho mỗi quan hệ này trở thành mối quan hệ 2 chiều . Mối quan hệ 2 chiều có nghĩa là chúng ta có thể truy cập các mặt hàng từ giỏ hàng và ngược lại

Thuộc tính mappedBy là thứ chúng ta sử dụng để Hibernate biết chúng ta đang sử dụng biến nào đại diện cho lớp cha trong lớp con của chúng ta

Dưới đây là các công nghệ và thư viện đưuọc sử dụng để phát triển một mẫu ứng dụng Hibernate mẫu triển khai liên kết một-nhiều

  • JDK 1.8 trở về sau
  • Hibernate 5
  • Maven 2 trở về sau
  • H2 , MySQL , Postgresql …

3. Cài đặt

3.1 Cài đặt cơ dở dữ liệu

Chúng ta sẽ sử dụng Hibernate để quản lý giản đồ của mình từ mô hình miền . Nói cách khác thì chúng ta không cần cung cấp các câu lệnh SQL để tạo các bảng và mối quan hệ khác nhau giữa các thực thể .Vì vậy , hãy chuyển sang tạo dự án ví dụ Hibernate

3.2 Maven Sự phụ thuộc (Dependencies)

Hãy bắt đầu bằng cách thêm các phụ thuộc trình điều khiển Hibernate và H2 vào tệp Pom.xml của chúng tôi . Phần phụ thuộc Hibernate sử dụng ghi nhật ký JBoss và nó tự động được thêm vào dưới dạng phụ thuộc bắc cầu :

  • Hibernate version 5.6.7.Final
  • H2 driver version 2.1.212

Vui lòng truy cập kho lưu trữ trung tâm MAven để biết các phiên bản mới nhất của Hibernate và các phụ thuộc của H2

3.2 Hibernate SessionFactory

Tiếp theo , hãy tạo Hibernate SessionFactory cho các tương tác với cơ sở dữ liệu của chúng tôi :

public static SessionFactory getSessionFactory() {

    ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder()
      .applySettings(dbSettings())
      .build();

    Metadata metadata = new MetadataSources(serviceRegistry)
      .addAnnotatedClass(Cart.class)
      // other domain classes
      .buildMetadata();

    return metadata.buildSessionFactory();
}

private static Map<String, String> dbSettings() {
    // return Hibernate settings
}

4. Các mô hình

Các cấu hình liền quan đến ánh xạ sẽ được thực hiện bằng cách sử dụng các chú thích JPA trong các lớp mô hình :

@Entity
@Table(name="CART")
public class Cart {

   //...

   @OneToMany(mappedBy="cart")
   private Set<Item> items;
   
   // getters and setters
}

Hãy lưu ý rằng chú thích @OneToMany được sử dụng để xác định thuộc tính trong lớp Item sẽ được sử dụng để ánh xạ biến mappedBy . Đó là lý do tại sao chúng ta có 1 thuộc tính có tên là “cart” trong lớp Item

@Entity
@Table(name="ITEMS")
public class Item {
   
   //...
   @ManyToOne
   @JoinColumn(name="cart_id", nullable=false)
   private Cart cart;

   public Item() {}
   
   // getters and setters
}

Cũng cần phải lưu ý rằng chú thích @ManyToOne được liên kết với biến lớp Cart . Chú thích @JoinColumn tham chiếu đến cốt đc ánh xạ

5. Hành động

Trong chương trình thử nghiệm , chúng tôi đang tạo một lớp với phương thức main() để nhận Hibernate Session và lưu các đối tượng mô hình vào cơ sở dữ liệu để thực hiện liên kết một - nhiều

sessionFactory = HibernateAnnotationUtil.getSessionFactory();
session = sessionFactory.getCurrentSession();
System.out.println("Session created");
       
tx = session.beginTransaction();

session.save(cart);
session.save(item1);
session.save(item2);
       
tx.commit();
System.out.println("Cart ID=" + cart.getId());
System.out.println("item1 ID=" + item1.getId()
 + ", Foreign Key Cart ID=" + item.getCart().getId());
System.out.println("item2 ID=" + item2.getId()
+ ", Foreign Key Cart ID=" + item.getCart().getId());

Và đây là kết quả của chương trình thử nghiệm trên của chúng tôi :

Session created
Hibernate: insert into CART values ()
Hibernate: insert into ITEMS (cart_id)
 values (?)
Hibernate: insert into ITEMS (cart_id)
 values (?)
Cart ID=7
item1 ID=11, Foreign Key Cart ID=7
item2 ID=12, Foreign Key Cart ID=7
Closing SessionFactory

6. Chú thích @ManyToOne

Như chúng ta có thể thấy ở trong phần thứ 2 , chúng ta có thể chỉ định mối quan hệ nhiều-một bằng cách sử dụng chú thích @ManyToOne . Ánh xạ nhiều - một có nghĩa là nhiều bản sao của thực thể này được ánh xạ tới một bản sao của thực thể khác

Chú thích @ManyToOne cũng cho phép chúng tôi tạo các mối quan hệ 2 chiều (Bidirectional)

6.1 Inconsistencies and Ownership (thiếu nhất quan - sự sở hữu)

Bây giờ , nếu Cart tham chiếu đến Item , nhưng Item không tham chiếu đến Cart , mối quan hệ này sẽ là một chiều . Các đối tượng cũng sẽ có một sự nhất quán tự nhiên
Tuy nhiên trong trường hợp mối quan hệ là 2 chiều thì dẫn đến khả năng mâu thuẫn

Bây giờ , hãy tưởng tượng một tình huống các dev muốn thêm một Item1 và Cart1 và một Item2 vào Cart2 nhưng mắc lỗi khiến các tham chiếu giữa Cart2 và Item2 trở nên không nhất quán

Cart cart1 = new Cart();
Cart cart2 = new Cart();

Item item1 = new Item(cart1);
Item item2 = new Item(cart2); 
Set<Item> itemsSet = new HashSet<Item>();
itemsSet.add(item1);
itemsSet.add(item2); 
cart1.setItems(itemsSet); // wrong!

Như ví dụ mà chúng tôi đưa ra wor trên thì khi Item2 tham chiếu đến Cart2 , trong khi Cart2 không tham chiếu đến Item2 điều này đã sinh ra lỗi

Vậy làm thế nào đế Hibernate lưu Item2 vào CSDL ? Tham chiếu khóa ngoại Item2 sẽ là Cart1 hay Cart2 ?

Chúng tối sẽ giải quyết vấn đề trên bằng cách sử dụng ý tưởng về một mặt riêng của mối quan hệ , các tham chiếu thuộc về phía sở hữu sẽ được ưu tiên hơn và được lưu vào cơ sở dữ liệu .

6.2 Item as the Owning Side

Như đã nếu trong phần đặc tả ở phần 2.9 , bạn nên đánh dấu bên nhiều - một là bên sở hữu

Nói một cách dễ hiểu hơn thì , Item là mặt sở hữu(owning side) và Cart là mặt nghịch đảo (inverse side)

Vậy làm thế nào chúng ta có thể làm được điều đó

Bằng cách chúng ta sửu dụng thuộc tính mappedBy trong lớp Cart , đánh dấu nó là phái nghịch đảo (inverse side)

Đồng thời chú thích trường Item.cart bằng @ManyToOne , biến Item trở thành bên sở hữu

Quay về ví dụ phái trên thì giờ đây Hibernate biết rằng tham chiếu của Item2 là quan trọng hơn và sẽ lưu tham chiếu cảu Item2 vào cơ sở dữ liệu

Hãy kiểm tra kết quả :

item1 ID=1, Foreign Key Cart ID=1
item2 ID=2, Foreign Key Cart ID=2

Mặc dù Cart tham chiếu đến Item2 trong đoạn mã trên , nhưng Item2 tham chiếu đến Cart2 đã được lưu trong CSDL

6.3 Cart as the Owning Side

Chúng ta có thể đánh dấu one-to-many là owning side (mặt sở hữu) và many-to-one là inverse side (mặt nghịch đảo)

Mặc dù đây là cách không được khuyến khích sử dụng nhưng chúng ta hãy tiếp tục và thử nó

public class ItemOIO {
    
    //  ...
    @ManyToOne
    @JoinColumn(name = "cart_id", insertable = false, updatable = false)
    private CartOIO cart;
    //..
}

public class CartOIO {
    
    //..  
    @OneToMany
    @JoinColumn(name = "cart_id") // we need to duplicate the physical information
    private Set<ItemOIO> items;
    //..
}

Lưu ý cách chúng tôi đã loại bỏ phần tử mappedBy và đặt @JoinColumn nhiều-một là có thể chèn và có thể cập nhật thành false.

Nếu chúng ta chạy đoạn mã sẽ cho ra kết quả ngược lại

item1 ID=1, Foreign Key Cart ID=1
item2 ID=2, Foreign Key Cart ID=1

7. Kết luận

Chúng tôi đã thấy việc triển khai mối quan hệ một-nhiều với cơ sở dữ liệu Hibernate ORM và H2 dễ dàng như thế nào bằng cách sử dụng các chú thích JPA.

Ngoài ra, chúng tôi đã tìm hiểu về mối quan hệ hai chiều và cách triển khai khái niệm phe sở hữu.

Mã nguồn trong bài viết này có thể được tìm thấy trên GitHub.

8. Ví dụ

Đây là ví dụ chi tiết về một mối quan hệ 2 chiều sử dụng các chú thích JPA

Controller

@RestController
public class APIController {

    @Autowired
    private CustomerAddressService customerAddressService ;

    @GetMapping("/customers")
    public ResponseEntity<List<Customer>> getAllCustomer() {
        List<Customer> result = customerAddressService.getAllCustomer();
        return ResponseEntity.ok().body(result);
    }
}

Model
Customer

@Data
@Entity(name = "Address")
@Table(name = "address")
public class Address {
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id ;

    @Column(name = "detail")
    private String detail ;

    public Address(String detail) {
        this.detail = detail;
    }

    @ManyToOne(fetch = FetchType.LAZY)
    @JsonIgnore
    private Customer customer;

    public Address() {
    }
}

@JosonIgnorechống gọi đệ quy từ con đến cha
Address

@Data
@Entity(name = "Address")
@Table(name = "address")
public class Address {
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id ;

    @Column(name = "detail")
    private String detail ;

    public Address(String detail) {
        this.detail = detail;
    }

    @ManyToOne(fetch = FetchType.LAZY)
    @JsonIgnore
    private Customer customer;

    public Address() {
    }
}

Service

@Service
public class CustomerAddressService {
    @Autowired
    private CustomerRepository customerRepository ;

    @Transactional
    public void generateCustomerAddress() {
        Customer cuong = new Customer("Trịnh Minh Cường");
        cuong.addAddress(new Address("14 ngõ 4 Nguyễn Đình Chiểu"));
        cuong.addAddress(new Address("tầng 12A, Viwaseen Tower, 48 Tố Hữu"));


        Customer dzung = new Customer("Đoàn Xuân Dũng");
        dzung.addAddress(new Address("Ngách 11, tổ 1, tập thể Bưu Điện"));

        Customer johnLennon = new Customer("John Lenon");
        johnLennon.addAddress(new Address("Empire State, New York"));

        customerRepository.save(johnLennon);
        customerRepository.save(cuong);
        customerRepository.save(dzung);
    }
    public List<Customer> getAllCustomer() {
        List<Customer> result = customerRepository.findAll();
        return result;
    }

Repository

@Repository
public interface CustomerRepository extends JpaRepository<Customer , Long> {
}

OneToManyApplication

@SpringBootApplication
public class OneToManyApplication implements CommandLineRunner {
    @Autowired private CustomerAddressService customerAddressService ;

    public static void main(String[] args) {
        SpringApplication.run(OneToManyApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        customerAddressService.generateCustomerAddress();
    }
}

Cấu hình application.properties

spring.datasource.url=jdbc:h2:mem:test
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=123
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=false
server.port=8081

Đây là cấu hình kết nối với CSDL H2