Tag: OneToOne

  • Ánh xạ quan hệ 1-1 sử dụng chia sẻ khoá chính trong Hibernate

    Ánh xạ quan hệ 1-1 sử dụng chia sẻ khoá chính trong Hibernate

    Các chương trình máy tính thể hiện các nhu cầu thực tế của con người, chúng ánh xạ các đối tượng trong thế giới thực thành các thực thể. Khi thực hiện quá trình ánh xạ đó, chúng ta thực hiện ánh xạ cả mối quan hệ giữa chúng. Trong bài viết này chúng ta đặt mối quan tâm tới các đối tượng có mối quan hệ 1-1 với nhau. Chúng ta sẽ cùng tìm hiểu cách chúng được thể hiện trong chương trình máy tính như thế nào.

    Các đối tượng trong thế giới thực được phản ánh trong chương trình máy tính như thế nào?

    Trước tiên chúng ta thấy các đối tượng sẽ được ánh xạ tương ứng thành các class trong các ngôn ngữ lập trình. Khi chúng được lưu trữ vào database, chúng sẽ được ánh xạ thành các bản ghi của một bảng. Vậy thì mối quan hệ giữa chúng được định nghĩa như thế nào? Đối với các bảng trong database, các khoá trong bảng sẽ thể hiện mối quan hệ giữa các bảng. Đối với quan hệ 1-1 chúng ta có thể định nghĩa theo 2 cách:

    • Sử dụng khoá ngoại duy nhất (một cột được đánh dấu là khoá ngoại và nó cũng là duy nhất trong bảng đó).
    • Hai bảng cùng chia sẻ khoá chính.

    Các đối tượng được phản ánh thành các bảng trong database

    Chúng ta cùng xem xét các thực thể được phản ánh thành các bảng trong database thông qua một vài ví dụ các bảng được thiết kế trong database như thế nào. Đối với cách sử dụng khoá ngoại duy nhất, các bảng có thể được định nghĩa như sau:

    Trong trường hợp bạn sử dụng cách chia sẻ khoá chính giữa hai bảng, các bảng trong database có thể được định nghĩa như sau:

    Các đối tượng được ánh xạ thành các class như thế nào?

    Đối với cách sử dụng khoá ngoại duy nhất, chúng ta có thể tham khảo cách định nghĩa mối quan hệ của chúng trong Spring Boot qua các bài viết sau:

    Trường hợp bạn sử dụng cách chia sẻ khoá chính giữa hai bảng chúng ta tìm hiểu qua từng bước dưới đây.

    Định nghĩa các bảng trong database

    Với ví dụ ở trên các bạn có thể sử dụng đoạn mã sau để tạo ra các bảng:

    CREATE TABLE IF NOT EXISTS `user` (
      `id` BIGINT NOT NULL AUTO_INCREMENT
      , `username` VARCHAR(255) UNIQUE
      , `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
      , `created_by` BIGINT DEFAULT NULL
      , `updated_at` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP
      , `updated_by` BIGINT DEFAULT NULL
      , `deleted_at` DATETIME DEFAULT NULL
      , `deleted_by` BIGINT DEFAULT NULL
      , PRIMARY KEY (`id`)
    );
    
    CREATE TABLE IF NOT EXISTS `user_info` (
      `user_id` BIGINT NOT NULL
      , `first_name` VARCHAR(255)
      , `last_name` VARCHAR(255)
      , PRIMARY KEY (`user_id`)
      , FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
    );
    

    Chúng ta có thể tham khảo cách migrate các bảng này bằng cách sử dụng Flyway trong bài viết Hướng dẫn migrate cơ sở dữ liệu sử dụng Flyway trong ứng dụng Spring Boot .

    Định nghĩa các entity để ánh xạ các bảng với các class

    Tiếp theo chúng ta cần định nghĩa các entity thành các class tương ứng. Từ đó, chúng ta có thể thực hiện các thao tác CRUD hoặc các thao tác truy vấn trên các bảng tương ứng.

    /**
     * <code>user_info</code>.
     *
     * @author Hieu Nguyen
     */
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Entity(name = "user_info")
    public class UserInfo {
    
      /** <code>user_id</code>. */
      @Id
      private Long userId;
    
      /** <code>first_name</code>. */
      private String firstName;
    
      /** <code>last_name</code>. */
      private String lastName;
    
      @MapsId
      @ToString.Exclude
      @PrimaryKeyJoinColumn
      @Fetch(FetchMode.JOIN)
      @OneToOne(cascade = CascadeType.PERSIST, optional = false, fetch = FetchType.EAGER)
      private User user;
    }
    
    /**
     * <code>user</code>.
     *
     *
     * @author Hieu Nguyen
     */
    @Data
    @Builder
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    @Entity(name = "user")
    @EqualsAndHashCode(onlyExplicitlyIncluded = true)
    public class User {
    
      /** <code>id</code>. */
      @Id
      @EqualsAndHashCode.Include
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
    
      /** <code>username</code>. */
      private String username;
    
      /** <code>created_at</code>. */
      private LocalDateTime createdAt;
    
      /** <code>created_by</code>. */
      private Long createdBy;
    
      /** <code>updated_at</code>. */
      private LocalDateTime updatedAt;
    
      /** <code>updated_by</code>. */
      private Long updatedBy;
    
      /** <code>deleted_at</code>. */
      private LocalDateTime deletedAt;
    
      /** <code>deleted_by</code>. */
      private Long deletedBy;
    
      /** <code>user_info.user_id</code> */
      @ToString.Exclude
      @OneToOne(mappedBy = "user", fetch = FetchType.EAGER)
      private UserInfo userInfo;
    }
    

    Chúng ta sử @Id để đánh dấu thuộc tính được ánh xạ tương ứng với trường khoá chính của bảng. Trong ví dụ này, chúng ta sử dụng trường tự tăng để sinh ra khoá chính cho bảng, do đó chúng ta sử dụng @GeneratedValue(strategy = GenerationType.IDENTITY) để thông báo với Hibernate rằng trường này sẽ được tự sinh trong database.

    Tiếp theo là phần quan trọng nhất, chúng ta sử dụng @MapsId để đánh dấu thuốc tính định nghĩa mối quan hệ 1-1 cùng với @OneToOne để xác định thực thể trong bảng có quan hệ 1-1 tương ứng. @MapsId sẽ thông báo cho Hibernate biết rằng chúng ta đang sử dụng khoá chính làm trường để thực hiện phép JOIN.

    Tiếp đến để thực hiện ánh xạ quan hệ 1-1 hai chiều, chúng ta sử dụng @OneToOne(mappedBy = "user", fetch = FetchType.EAGER) để đánh dấu thuộc tính ánh xạ sang thực thể nguồn đã được định nghĩa ở trên. Thuộc tính mappedBy chính là tên thuộc tính được khai báo với @MapsId ở trên.

    Xác nhận việc định nghĩa quan hệ 1-1

    Tiếp theo chúng ta cùng viết một đoạn chương trình nhỏ để kiểm tra lại các bước đã thực hiện ở trên.

    /**
     * Main.
     *
     * @author Hieu Nguyen
     */
    @Component
    @RequiredArgsConstructor
    public class Main implements CommandLineRunner {
    
      private final UserRepository userRepository;
    
      @Override
      @Transactional
      public void run(String... args) throws Exception {
        var uuid = UUID.randomUUID();
        var userInfo = UserInfo.builder().firstName("Hieu-" + uuid).lastName("Nguyen-" + uuid).build();
        var user = User.builder().username("hieunv-" + UUID.randomUUID()).userInfo(userInfo).build();
        userInfo.setUser(user);
        userRepository.save(user);
      }
    }
    

    Chạy thử đoạn chương trình này chúng ta sẽ nhận được output như sau:

    2023-02-15 18:56:38.262 DEBUG 25263 --- [           main] org.hibernate.SQL                        : 
        insert 
        into
            user
            (created_at, created_by, deleted_at, deleted_by, passport_id, updated_at, updated_by, username) 
        values
            (?, ?, ?, ?, ?, ?, ?, ?)
    Hibernate: 
        insert 
        into
            user
            (created_at, created_by, deleted_at, deleted_by, passport_id, updated_at, updated_by, username) 
        values
            (?, ?, ?, ?, ?, ?, ?, ?)
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [TIMESTAMP] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [TIMESTAMP] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [5] as [BIGINT] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [6] as [TIMESTAMP] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [7] as [BIGINT] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [8] as [VARCHAR] - [hieunv-48779f8f-4efe-4d15-b658-92ebd3f7d9a3]
    2023-02-15 18:56:38.278 DEBUG 25263 --- [           main] org.hibernate.SQL                        : 
        insert 
        into
            user_info
            (first_name, last_name, user_id) 
        values
            (?, ?, ?)
    Hibernate: 
        insert 
        into
            user_info
            (first_name, last_name, user_id) 
        values
            (?, ?, ?)
    2023-02-15 18:56:38.278 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Hieu-3c8726d4-0d3f-49f6-ba05-819bbb428863]
    2023-02-15 18:56:38.278 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Nguyen-3c8726d4-0d3f-49f6-ba05-819bbb428863]
    2023-02-15 18:56:38.278 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [9]
    

    Chúng ta thấy rằng có 2 bản ghi đã được insert vào 2 bảng chúng ta đã định nghĩa ở trên.

    Tổng kết

    Trong bài viết này chúng ta đã cùng đi từ các khái niệm cơ bản liên quan đến quan hệ 1-1 cũng như cách triển khai quan hệ này với MySQL database. Sau đó chúng ta cũng viết một ứng dụng đơn giản bằng Spring Boot để minh hoạ cơ chế hoạt động của quan hệ này.

  • Ánh xạ one-to-one bidirectional trong Hibernate sử dụng khoá ngoại

    Ánh xạ one-to-one bidirectional trong Hibernate sử dụng khoá ngoại

    Chúng ta đã cùng tìm hiểu về ánh xạ one-to-one unidirectional. Chúng ta cũng đã đề cập tới one-to-one bidirectional. Với one-tone-unidirectional chúng ta đã có thể truy cập thực thể đích từ thực thể nguồn nhưng không thể truy cập ngược lại. Trong bài viết này chúng ta sẽ cùng tìm hiểu những hạn chế của quan hệ unidirectional và tại sao chúng ta cần quan hệ bidirectional.

    Những hạn chế của one-to-one unidirectional

    Trong bài viết trước chúng ta đã định nghĩa hai thực thể CustomerAccount có quan hệ thông qua khoá ngoại ACCOUNT_ID. Nếu bằng cách nào đó, chúng ta chỉ xoá thực thể Account và để nguyên thực thể Customer, khi đó khoá ngoại trong bảng Customer sẽ tham chiếu tới một đối tượng không tồn tại, vấn đề này còn được gọi là dangling foreign key. Tuỳ chọn xoá thực thể Customer khi thực thị Account bị xoá phụ thuộc vào thiết kế cơ dữ liệu, đôi khi chúng ta muốn giữ lại thực thể Customer dưới dạng thông tin lưu trữ để theo dõi lịch sử. Chúng ta có thể làm điều này mà không cần thay đổi cơ sở dữ liệu bằng cách thay đổi thực thể Account

    /**
     * <code>account</code>.
     *
     * @author Hieu Nguyen
     */
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Entity(name = "account")
    @ToString(onlyExplicitlyIncluded = true)
    @EqualsAndHashCode(onlyExplicitlyIncluded = true)
    public class Account {
    
      /** <code>id</code>. */
      @Id
      @ToString.Include
      @EqualsAndHashCode.Include
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
    
      /** <code>username</code>. */
      @ToString.Include private String username;
    
      @OneToOne(mappedBy = "account", cascade = CascadeType.ALL)
      private Customer customer;
    }
    

    Ở thực thể Account chúng ta không có cột nào có thể sử dụng để tham chiếu tới thực thể Customer. Do đó chúng ta cần đến sự hỗ trợ từ Hibernate.

    Cài đặt one-to-one bidirectional

    Chúng ta chỉ cần thêm đoạn mã sau vào thực thể Account:

      ...
      @OneToOne(mappedBy = "account", cascade = CascadeType.ALL)
      private Customer customer;
      ...
    

    Định nghĩa mappedBy = "account" sẽ thông báo cho Hibernate biết rằng nó cần tìm thuộc tính account trong thực thể Customer và liên kết thực thể cụ thể đó với đối tượng Account. Bây giờ chúng ta cùng thêm một thực thể vào database nhưng lúc này chúng ta sẽ save thực thể Account và thực thể Customer cũng sẽ được thêm vào database vì chúng ta đã sử dụng cascade = CascadeType.ALL.

    @Component
    public class Main implements CommandLineRunner {
      @Autowired private AccountRepository accountRepository;
      @Autowired private CustomerRepository customerRepository;
    
      @Override
      @Transactional
      public void run(String... args) throws Exception {
        var customer = Customer.builder().firstName("Hieu").lastName("Nguyen").build();
        var account = Account.builder().username("hieunv").customer(customer).build();
        customer.setAccount(account);
        accountRepository.save(account);
      }
    }
    

    Sau khi chạy đoạn mã trên, chúng ta thấy rằng có 2 bản ghi đã được insert vào database. Chúng ta cũng sẽ thấy ouput như sau:

    2023-02-07 13:05:42.746 DEBUG 90289 --- [           main] o.h.e.t.internal.TransactionImpl         : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
    2023-02-07 13:05:42.746 DEBUG 90289 --- [           main] o.h.e.t.internal.TransactionImpl         : begin
    2023-02-07 13:05:42.764 DEBUG 90289 --- [           main] org.hibernate.engine.spi.ActionQueue     : Executing identity-insert immediately
    2023-02-07 13:05:42.767 DEBUG 90289 --- [           main] org.hibernate.SQL                        : insert into account (username) values (?)
    Hibernate: insert into account (username) values (?)
    2023-02-07 13:05:42.769 TRACE 90289 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [hieunv]
    2023-02-07 13:05:42.778 DEBUG 90289 --- [           main] o.h.id.IdentifierGeneratorHelper         : Natively generated identity: 2
    2023-02-07 13:05:42.778 DEBUG 90289 --- [           main] o.h.r.j.i.ResourceRegistryStandardImpl   : HHH000387: ResultSet's statement was not registered
    2023-02-07 13:05:42.779 DEBUG 90289 --- [           main] org.hibernate.engine.spi.ActionQueue     : Executing identity-insert immediately
    2023-02-07 13:05:42.779 DEBUG 90289 --- [           main] org.hibernate.SQL                        : insert into customer (account_id, first_name, last_name) values (?, ?, ?)
    Hibernate: insert into customer (account_id, first_name, last_name) values (?, ?, ?)
    2023-02-07 13:05:42.779 TRACE 90289 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [2]
    2023-02-07 13:05:42.779 TRACE 90289 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Hieu]
    2023-02-07 13:05:42.779 TRACE 90289 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [Nguyen]
    2023-02-07 13:05:42.792 DEBUG 90289 --- [           main] o.h.id.IdentifierGeneratorHelper         : Natively generated identity: 2
    2023-02-07 13:05:42.792 DEBUG 90289 --- [           main] o.h.r.j.i.ResourceRegistryStandardImpl   : HHH000387: ResultSet's statement was not registered
    2023-02-07 13:05:42.792 DEBUG 90289 --- [           main] o.h.e.t.internal.TransactionImpl         : committing
    2023-02-07 13:05:42.793 DEBUG 90289 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Processing flush-time cascades
    2023-02-07 13:05:42.793 DEBUG 90289 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Dirty checking collections
    2023-02-07 13:05:42.794 DEBUG 90289 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 insertions, 0 updates, 0 deletions to 2 objects
    2023-02-07 13:05:42.794 DEBUG 90289 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
    2023-02-07 13:05:42.795 DEBUG 90289 --- [           main] o.hibernate.internal.util.EntityPrinter  : Listing entities:
    2023-02-07 13:05:42.795 DEBUG 90289 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Account{id=2, username=hieunv, customer=app.demo.entity.Customer#2}
    2023-02-07 13:05:42.795 DEBUG 90289 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Customer{firstName=Hieu, lastName=Nguyen, id=2, account=app.demo.entity.Account#2}
    

    Nhìn vào ouput trên chúng ta thấy rằng có 2 bản ghi đã được insert vào database với id=2.

    Truy xuất thông tin Customer từ thực thể Account

    Bây giờ chúng ta cùng tìm hiểu xem bằng cách nào Hibernate có thể lấy thông tin Customer thông qua thực thể Account. Chúng ta cùng cài đặt thử đoạn mã sau:

    @Component
    @Slf4j
    public class Main implements CommandLineRunner {
      @Autowired private AccountRepository accountRepository;
      @Autowired private CustomerRepository customerRepository;
    
      @Override
      @Transactional
      public void run(String... args) throws Exception {
        log.info("{}", accountRepository.findById(2L).get().getCustomer());
      }
    }
    

    Sau khi chạy chương trình các bạn sẽ thấy output:

    2023-02-07 13:17:51.141 DEBUG 90956 --- [           main] o.h.e.t.internal.TransactionImpl         : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
    2023-02-07 13:17:51.141 DEBUG 90956 --- [           main] o.h.e.t.internal.TransactionImpl         : begin
    2023-02-07 13:17:51.157 DEBUG 90956 --- [           main] org.hibernate.SQL                        : select account0_.id as id1_0_0_, account0_.username as username2_0_0_, customer1_.id as id1_1_1_, customer1_.account_id as account_4_1_1_, customer1_.first_name as first_na2_1_1_, customer1_.last_name as last_nam3_1_1_ from account account0_ left outer join customer customer1_ on account0_.id=customer1_.account_id where account0_.id=?
    Hibernate: select account0_.id as id1_0_0_, account0_.username as username2_0_0_, customer1_.id as id1_1_1_, customer1_.account_id as account_4_1_1_, customer1_.first_name as first_na2_1_1_, customer1_.last_name as last_nam3_1_1_ from account account0_ left outer join customer customer1_ on account0_.id=customer1_.account_id where account0_.id=?
    2023-02-07 13:17:51.159 TRACE 90956 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [2]
    2023-02-07 13:17:51.170 DEBUG 90956 --- [           main] l.p.e.p.i.EntityReferenceInitializerImpl : On call to EntityIdentifierReaderImpl#resolve, EntityKey was already known; should only happen on root returns with an optional identifier specified
    2023-02-07 13:17:51.173 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Resolving attributes for [app.demo.entity.Account#2]
    2023-02-07 13:17:51.173 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `username` : value = hieunv
    2023-02-07 13:17:51.173 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`username`)  - enhanced for lazy-loading? - false
    2023-02-07 13:17:51.173 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `customer` : value = 2
    2023-02-07 13:17:51.173 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`customer`)  - enhanced for lazy-loading? - false
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Done materializing entity [app.demo.entity.Account#2]
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Resolving attributes for [app.demo.entity.Customer#2]
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `account` : value = 2
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`account`)  - enhanced for lazy-loading? - false
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `firstName` : value = Hieu
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`firstName`)  - enhanced for lazy-loading? - false
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `lastName` : value = Nguyen
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`lastName`)  - enhanced for lazy-loading? - false
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Done materializing entity [app.demo.entity.Customer#2]
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] .l.e.p.AbstractLoadPlanBasedEntityLoader : Done entity load : app.demo.entity.Account#2
    2023-02-07 13:17:51.175  INFO 90956 --- [           main] app.demo.Main                            : Customer(id=2, firstName=Hieu, lastName=Nguyen)
    2023-02-07 13:17:51.175 DEBUG 90956 --- [           main] o.h.e.t.internal.TransactionImpl         : committing
    2023-02-07 13:17:51.175 DEBUG 90956 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Processing flush-time cascades
    2023-02-07 13:17:51.178 DEBUG 90956 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Dirty checking collections
    2023-02-07 13:17:51.179 DEBUG 90956 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 insertions, 0 updates, 0 deletions to 2 objects
    2023-02-07 13:17:51.179 DEBUG 90956 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
    2023-02-07 13:17:51.179 DEBUG 90956 --- [           main] o.hibernate.internal.util.EntityPrinter  : Listing entities:
    2023-02-07 13:17:51.179 DEBUG 90956 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Account{id=2, username=hieunv, customer=app.demo.entity.Customer#2}
    2023-02-07 13:17:51.179 DEBUG 90956 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Customer{firstName=Hieu, lastName=Nguyen, id=2, account=app.demo.entity.Account#2}
    
    

    Các bạn chú ý tới dòng sau:

    2023-02-07 13:17:51.157 DEBUG 90956 --- [           main] org.hibernate.SQL                        : select account0_.id as id1_0_0_, account0_.username as username2_0_0_, customer1_.id as id1_1_1_, customer1_.account_id as account_4_1_1_, customer1_.first_name as first_na2_1_1_, customer1_.last_name as last_nam3_1_1_ from account account0_ left outer join customer customer1_ on account0_.id=customer1_.account_id where account0_.id=?
    Hibernate: select account0_.id as id1_0_0_, account0_.username as username2_0_0_, customer1_.id as id1_1_1_, customer1_.account_id as account_4_1_1_, customer1_.first_name as first_na2_1_1_, customer1_.last_name as last_nam3_1_1_ from account account0_ left outer join customer customer1_ on account0_.id=customer1_.account_id where account0_.id=?
    

    Các bạn có thể thấy rằng thực thể Customer có thể đường truy xuất thông qua câu lệnh LEFT OUTER JOIN. Chúng ta cũng có thể thực hiện các thao tác updatedelete theo cùng cách như đã thực hiện với one-to-one unidirectional.

    Tổng kết

    Trong bài viết này chúng ta đã chỉ ra những vấn đề đối với quan hệ one-to-one unidirectional và cách triển khai quan hệ one-to-one bidirectional trong Hibernate để giải quyết các vấn đề với one-to-one unidirectional.

  • Ánh xạ one-to-one unidirectional trong Hibernate sử dụng khoá ngoại

    Ánh xạ one-to-one unidirectional trong Hibernate sử dụng khoá ngoại

    Hibernate là một framework cung cấp một số lớp trừu tượng, nghĩa là lập trình viên không phải lo lắng về việc triển khai, Hibernate tự thực hiện các triển khai bên trong nó như thiết lập một kết nối cơ sở dữ liệu, viết các truy vấn để thực hiện các thao tác CRUD, … Nó là một java framework được sử dụng để phát triển persistence logic. Persistence logic có nghĩa là lưu trữ và xử lí dữ liệu để sử dụng lâu dài. Chính xác hơn *Hibernate * là một framework ORM (Object Relational Mapping) mã nguồn mở để phát triển các đối tượng độc lập với các phần mềm cơ sở dữ liệu và tạo ra persistence logic độc lập với Java, J2EE.

    Ánh xạ one-to-one là gì?

    Anh xạ one-to-one thể hiện rằng một thực thể duy nhất có mối liên kết với một thể hiện duy nhất của một thực thể khác. Một thể hiện của thực thể nguồn có thể được ánh xạ tới nhiều nhất một thể hiện của thực thể đích. Một số ví dụ minh hoạ ánh xạ one-to-one:

    • Mỗi người chỉ có duy nhất một hộ chiếu, một hộ chiếu chỉ được liên kết với duy nhất một người.
    • Mỗi con báo có một mẫu đốm độc nhất, một mẫu đốm chỉ được liên kết với duy nhất một con báo.
    • Mỗi chúng ta có một định danh duy nhất ở trường đại học, mỗi định danh dược liên kết với một người duy nhất.

    Trong các hệ quản trị cơ sở dữ liệu, ánh xạ one-to-one thường có hai kiểu:

    • one-to-one unidirectional
    • one-to-one bidirectional

    one-to-one unidirectional

    Ở kiểu ánh xạ này một thực thể có một thuộc tính hoặc một cột tham chiếu tới một thuộc tính hoặc một cột ở thực thể đích. Chúng ta cùng xem ví dụ sau:

    Bảng customer tham chiếu tới bảng account thông qua khoá ngoại ACCOUNT_ID. Thực thể đích (account) không có cách nào tham chiếu tới bảng customer nhưng bảng customer có thể truy cập tới bảng account thông qua khoá ngoại. Quan hệ trên được sinh ra bởi kịch bản SQL sau:

    CREATE TABLE IF NOT EXISTS `ACCOUNT` (
      `ID` BIGINT NOT NULL AUTO_INCREMENT
      , `USERNAME` VARCHAR(255) UNIQUE
      , PRIMARY KEY (`ID`)
    );
    
    CREATE TABLE IF NOT EXISTS `CUSTOMER` (
      `ID` BIGINT NOT NULL AUTO_INCREMENT
      , `FIRST_NAME` VARCHAR(255) NULL DEFAULT NULL
      , `LAST_NAME` VARCHAR(255) NULL DEFAULT NULL
      , `ACCOUNT_ID` BIGINT NOT NULL UNIQUE
      , PRIMARY KEY (`ID`)
      , FOREIGN KEY (`ACCOUNT_ID`)
        REFERENCES `demo`.`ACCOUNT`(`ID`) ON DELETE NO ACTION ON UPDATE NO ACTION
    );
    

    Khi tạo bảng customer chúng ta tham chiếu khoá chính trong bảng account (account_id). Chúng ta cố tình đặt ON DELETE NO ACTIONON UPDATE NO ACTION vì chúng ta sẽ đặt các giá trị này bên trong Hibernate. Bây giờ chúng ta sẽ thực hiện migration kịch bản này bằng Flyway. Tham khảo Hướng dẫn migrate cơ sở dữ liệu sử dụng Flyway trong ứng dụng Spring Boot.

    Trước khi bắt đầu định nghĩa các thực thể, chúng ta cần thêm các thư viện cần thiết. Tham khảo Hướng dẫn sử dụng Spring Boot với Hibernate để thực hiện các thao tác cần thiết.

    Định nghĩa Hibernate entity

    Bây giờ chúng ta đã có thể định nghĩa các thực thể Hibernate.

    /**
     * <code>account</code>.
     *
     * @author Hieu Nguyen
     */
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Entity(name = "account")
    @ToString(onlyExplicitlyIncluded = true)
    @EqualsAndHashCode(onlyExplicitlyIncluded = true)
    public class Account {
    
      /** <code>id</code>. */
      @Id
      @ToString.Include
      @EqualsAndHashCode.Include
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
    
      /** <code>username</code>. */
      @ToString.Include private String username;
    }
    
    /**
     * <code>customer</code>.
     *
     * @author Hieu Nguyen
     */
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Entity(name = "customer")
    @ToString(onlyExplicitlyIncluded = true)
    @EqualsAndHashCode(onlyExplicitlyIncluded = true)
    public class Customer {
    
      /** <code>id</code>. */
      @Id
      @ToString.Include
      @EqualsAndHashCode.Include
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
    
      /** <code>first_name</code> */
      @ToString.Include private String firstName;
    
      /** <code>last_name</code>. */
      @ToString.Include private String lastName;
    
      /** <code>account_id</code>. */
      @OneToOne(optional = false, cascade = CascadeType.ALL)
      @JoinColumn(name = "ACCOUNT_ID", unique = true, nullable = false, updatable = false)
      private Account account;
    }
    

    Chúng ta sử dụng @Entity annotation để định nghĩa Hibernate entity. Tên bảng tương ứng với entity được định nghĩa thông quan thuộc tính name của @Entity annotation hoặc có thể sử dụng @Table(name = "account") để định nghĩa tên bảng. Chúng ta cùng xem xét một số annotation khác:

    • @Id annotation định nghĩa trường tưng ứng là khoá chính của entity.
    • @GeneratedValue annotation định nghĩa chiến lược sinh giá trị cho khoá chính, chúng ta sử dụng strategy = GenerationType.IDENTITY để xác định khoá chính sẽ được sinh tự động trong cơ sở dữ liệu (cột tương ứng trong cơ sở dữ liệu được đánh dấu là AUTO_INCREMENT).
    • @Column annotation định nghĩa tên cột tương ứng trong cơ sở dữ liệu.

    Triển khai ánh xạ one-to-one

    Phần chính mà chúng ta cần chú ý tới:

      @OneToOne(optional = false, cascade = CascadeType.ALL)
      @JoinColumn(name = "ACCOUNT_ID", unique = true, nullable = false, updatable = false)
      private Account account;
    

    Đối tượng Account được thêm vào bên trong class Customer và được đánh dấu với @OneToOne annotation để xác định đây ánh xạ one-to-one. Annotation cũng chưa thuộc tính cascade xác định chiến lược cascading. Cascading là một tính năng của Hibernate được sử dụng để quản lí trạng thái của thực thể đích mỗi khi trạng thái của thực thể cha thay đổi. Hibernate có các kiểu cascading sau:

    • CascadeType.ALL – lan truyền tất cả các thao tác từ thực thể cha sang thực thể đích.
    • CascadeType.PERSIST – lan truyền thao tác persist từ thực thể cha sang thực thể đích.
    • CascadeType.MERGE – lan truyền thao tác merge từ thực thể cha sang thực thể đích.
    • CascadeType.REMOVE – lan truyền thao tác remove từ thực thể cha sang thực thể đích.
    • CascadeType.REFRESH – lan truyền thao tác refresh từ thực thể cha sang thực thể đích.
    • CascadeType.DETACH – lan truyền thao tác detach từ thực thể cha sang thực thể đích.

    Ví dụ nếu cascade = CascadeType.REMOVE thì nếu thực thể cha bị xoá khởi cơ sở dữ liệu thì thực thể đích cũng bị xoá khỏi cơ sở dữ liệu. Trong trường hợp của chúng ta nếu thực thể Customer bị xoá khởi cơ sở dữ liệu thì thực thể liên quan Account cũng bị xoá khỏi cơ sở dữ liệu.

    @JoinColumn được xử dụng để xác định tên cột được sử dụng để tìm kiến thực thể đích. Thực thể Account sẽ được tìm kiến thông qua cột ACCOUNT_ID, nó chính xác là khoá ngoại của bảng customer mà chúng ta định nghĩa ở trên.

    Sử dụng Hibernate entity để lưu dữ liệu vào cơ sở dữ liệu

    Chúng ta cùng tạo một đoạn mã đơn giản để kiểm tra lại toàn bộ định nghĩa đã tạo ở trên:

    @Component
    public class Main implements CommandLineRunner {
      @Autowired private CustomerRepository customerRepository;
    
      @Override
      @Transactional
      public void run(String... args) throws Exception {
        var account = Account.builder().username("hieunv").build();
        var customer = Customer.builder().firstName("Hieu").lastName("Nguyen").account(account).build();
        customerRepository.save(customer);
      }
    }
    

    Sau khi thực thi chương trình chúng ta sẽ thấy output sau:

    2023-02-06 20:28:55.358 DEBUG 78531 --- [           main] o.h.e.t.internal.TransactionImpl         : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
    2023-02-06 20:28:55.358 DEBUG 78531 --- [           main] o.h.e.t.internal.TransactionImpl         : begin
    2023-02-06 20:28:55.378 DEBUG 78531 --- [           main] org.hibernate.engine.spi.ActionQueue     : Executing identity-insert immediately
    2023-02-06 20:28:55.381 DEBUG 78531 --- [           main] org.hibernate.SQL                        : insert into account (username) values (?)
    Hibernate: insert into account (username) values (?)
    2023-02-06 20:28:55.383 TRACE 78531 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [hieunv]
    2023-02-06 20:28:55.394 DEBUG 78531 --- [           main] o.h.id.IdentifierGeneratorHelper         : Natively generated identity: 1
    2023-02-06 20:28:55.394 DEBUG 78531 --- [           main] o.h.r.j.i.ResourceRegistryStandardImpl   : HHH000387: ResultSet's statement was not registered
    2023-02-06 20:28:55.395 DEBUG 78531 --- [           main] org.hibernate.engine.spi.ActionQueue     : Executing identity-insert immediately
    2023-02-06 20:28:55.395 DEBUG 78531 --- [           main] org.hibernate.SQL                        : insert into customer (account_id, first_name, last_name) values (?, ?, ?)
    Hibernate: insert into customer (account_id, first_name, last_name) values (?, ?, ?)
    2023-02-06 20:28:55.396 TRACE 78531 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
    2023-02-06 20:28:55.396 TRACE 78531 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Hieu]
    2023-02-06 20:28:55.396 TRACE 78531 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [Nguyen]
    2023-02-06 20:28:55.400 DEBUG 78531 --- [           main] o.h.id.IdentifierGeneratorHelper         : Natively generated identity: 1
    2023-02-06 20:28:55.400 DEBUG 78531 --- [           main] o.h.r.j.i.ResourceRegistryStandardImpl   : HHH000387: ResultSet's statement was not registered
    2023-02-06 20:28:55.400 DEBUG 78531 --- [           main] o.h.e.t.internal.TransactionImpl         : committing
    2023-02-06 20:28:55.401 DEBUG 78531 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Processing flush-time cascades
    2023-02-06 20:28:55.401 DEBUG 78531 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Dirty checking collections
    2023-02-06 20:28:55.403 DEBUG 78531 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 insertions, 0 updates, 0 deletions to 2 objects
    2023-02-06 20:28:55.403 DEBUG 78531 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
    2023-02-06 20:28:55.403 DEBUG 78531 --- [           main] o.hibernate.internal.util.EntityPrinter  : Listing entities:
    2023-02-06 20:28:55.404 DEBUG 78531 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Account{id=1, username=hieunv}
    2023-02-06 20:28:55.404 DEBUG 78531 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Customer{firstName=Hieu, lastName=Nguyen, id=1, account=app.demo.entity.Account#1}
    

    Kiểm tra trong cơ sở dữ liệu chúng ta sẽ thấy các bản ghi sau đã được insert vào trong database.

    Xoá dữ liệu cascading

    Tiếp theo chúng ta cùng xem một đoạn mã để kiểm chứng cơ chế hoạt động cascading. Chúng ta sẽ thử xoá thực thể Customer để xem thực thể Account tương ứng sẽ được xử lí như thế nào. Chúng ta cùng xem đoạn mã sau:

    @Component
    public class Main implements CommandLineRunner {
      @Autowired private CustomerRepository customerRepository;
    
      @Override
      @Transactional
      public void run(String... args) throws Exception {
        var customer = customerRepository.findById(1L).orElseThrow(() -> new EntityNotFoundException());
        customerRepository.delete(customer);
      }
    }
    

    Sau khi chạy chương trình chúng ta sẽ thấy output như sau:

    2023-02-07 09:45:07.300 DEBUG 83225 --- [           main] o.h.e.t.internal.TransactionImpl         : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
    2023-02-07 09:45:07.300 DEBUG 83225 --- [           main] o.h.e.t.internal.TransactionImpl         : begin
    2023-02-07 09:45:07.317 DEBUG 83225 --- [           main] org.hibernate.SQL                        : select customer0_.id as id1_1_0_, customer0_.account_id as account_4_1_0_, customer0_.first_name as first_na2_1_0_, customer0_.last_name as last_nam3_1_0_, account1_.id as id1_0_1_, account1_.username as username2_0_1_ from customer customer0_ inner join account account1_ on customer0_.account_id=account1_.id where customer0_.id=?
    Hibernate: select customer0_.id as id1_1_0_, customer0_.account_id as account_4_1_0_, customer0_.first_name as first_na2_1_0_, customer0_.last_name as last_nam3_1_0_, account1_.id as id1_0_1_, account1_.username as username2_0_1_ from customer customer0_ inner join account account1_ on customer0_.account_id=account1_.id where customer0_.id=?
    2023-02-07 09:45:07.319 TRACE 83225 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
    2023-02-07 09:45:07.324 DEBUG 83225 --- [           main] l.p.e.p.i.EntityReferenceInitializerImpl : On call to EntityIdentifierReaderImpl#resolve, EntityKey was already known; should only happen on root returns with an optional identifier specified
    2023-02-07 09:45:07.328 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Resolving attributes for [app.demo.entity.Customer#1]
    2023-02-07 09:45:07.328 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `account` : value = 1
    2023-02-07 09:45:07.328 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`account`)  - enhanced for lazy-loading? - false
    2023-02-07 09:45:07.328 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `firstName` : value = Hieu
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`firstName`)  - enhanced for lazy-loading? - false
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `lastName` : value = Nguyen
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`lastName`)  - enhanced for lazy-loading? - false
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Done materializing entity [app.demo.entity.Customer#1]
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Resolving attributes for [app.demo.entity.Account#1]
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `username` : value = hieunv
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`username`)  - enhanced for lazy-loading? - false
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Done materializing entity [app.demo.entity.Account#1]
    2023-02-07 09:45:07.330 DEBUG 83225 --- [           main] .l.e.p.AbstractLoadPlanBasedEntityLoader : Done entity load : app.demo.entity.Customer#1
    2023-02-07 09:45:07.334 DEBUG 83225 --- [           main] o.h.e.t.internal.TransactionImpl         : committing
    2023-02-07 09:45:07.334 DEBUG 83225 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Processing flush-time cascades
    2023-02-07 09:45:07.334 DEBUG 83225 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Dirty checking collections
    2023-02-07 09:45:07.335 DEBUG 83225 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 insertions, 0 updates, 2 deletions to 2 objects
    2023-02-07 09:45:07.335 DEBUG 83225 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
    2023-02-07 09:45:07.335 DEBUG 83225 --- [           main] o.hibernate.internal.util.EntityPrinter  : Listing entities:
    2023-02-07 09:45:07.336 DEBUG 83225 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Customer{firstName=Hieu, lastName=Nguyen, id=1, account=app.demo.entity.Account#1}
    2023-02-07 09:45:07.336 DEBUG 83225 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Account{id=1, username=hieunv}
    2023-02-07 09:45:07.341 DEBUG 83225 --- [           main] org.hibernate.SQL                        : delete from customer where id=?
    Hibernate: delete from customer where id=?
    2023-02-07 09:45:07.341 TRACE 83225 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
    2023-02-07 09:45:07.346 DEBUG 83225 --- [           main] org.hibernate.SQL                        : delete from account where id=?
    Hibernate: delete from account where id=?
    2023-02-07 09:45:07.346 TRACE 83225 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
    

    Chúng ta thấy rằng khi thực thể Customer bị xoá thì thực thể Account tương ứng được liên kết thông quan anh xạ one-to-one cũng bị xoá.

    Tổng kết

    Chúng ta đã tiến hành cài đặt ánh xạ one-to-one unidirectional. Thực thể Customer có thể truy cập vào thực thể Account nhưng chúng ta không thể thực hiện ngược lại. Trong thực tế thì chúng ta cần truy cập được thực thể Customer từ thực thể Account. Do đó chúng ta cần đến anh xạ one-to-one bidirectional. Chúng ta cùng xem xét trong bài viết tiếp theo nhé.