Nếu được hỏi về ORM giải quyết vấn đề gì? Bạn sẽ trả lời như thế nào?

Trong vài thập kỷ, các nhà phát triển ứng dụng trên khắp thế giới thường nhắc đến một thuật ngữ paradigm mismatch ám chỉ đến việc không tương thích giữa mô hình lập trình hướng đối tượng và mô hình dữ liệu quan hệ. Một ứng dụng sử dụng cả 2 mô hình này cần giải quyết các vấn đề không tương thích giữa chúng. Trong bài viết này chúng ta sẽ tập trung tìm hiểu xem các vấn đề này là gì nhé.

Các vấn đề ORM giải quyết

Việc không tương thích giữa mô hình lập trình hướng đối tượng và mô hình dữ liệu quan hệ có thể chia ra thành phần nhỏ khác nhau. Đầu tiên mình sẽ lấy ví dụ để làm rõ vấn đề trong những phần tiếp theo, giả sử mình có một ứng dụng thương mại điện tử bao gồm thông tin của người dùng và các hóa đơn của họ.

orm-resolve-problem

Trong mô hình trên chúng ta có thể thấy User có nhiều BillingDetails. Dựa vào đó chúng ta có thể ánh xạ thành 2 class trong Java như sau

public class User {
    private String username;
    private String address;
    Set<BillingDetails> billingDetailsSet;
    // Accessor methods (getter/setter), business methods, etc.
}

public class BillingDetails {
    String account;
    String bankname;
    User user;
    // Accessor methods (getter/setter), business methods, etc.
}

Lưu ý rằng trong bài viết này chúng ta chỉ quan tâm đến các thuộc tính ánh xạ xuống cơ sở dữ liệu, tạm thời bỏ qua các xử lý logic của ứng dụng. Tương ứng chúng ta sẽ có 2 bảng dưới database được tạo với câu lệnh SQL 

create table USERS (
    USERNAME varchar(15) not null primary key,
    ADDRESS varchar(255) not null
);

create table BILLINGDETAILS (
    ACCOUNT varchar(15) not null primary key,
    BANKNAME varchar(255) not null,
    USERNAME varchar(15) not null,
    foreign key (USERNAME) references USERS
);

Với mô hình dữ liệu đơn như trên thì rất khó để chúng ta tìm ra các vấn đề trong đó. Ứng dụng với mô hình này hoàn toàn có thể sử dụng JDBC để thao tác với CSDL. Nhưng từ từ đã, nếu ứng dụng mở rộng hơn và chúng ta có thêm nhiều bảng hơn thì sao nhỉ?

Granularity Problem – Độ Chi Tiết

Chúng ta có thể thấy vấn đề này rõ ràng nhất từ thuộc tính address được thiết kế kiểu dữ String. Thông thường các hệ thống sẽ yêu cầu lưu trữ địa chỉ thành từng cột riêng lẽ như street, city, country, và Zipcode. Chúng ta có thể thêm các thuộc tính này vào User class thế nhưng rất có thể những class khác trong hệ thống cũng cần đến những thông tin này. Vì vậy để tránh bị code bị trùng lặp chúng ta sẽ tạo ra một Address class chứa các thông tin trên.

orm-resolve-problem-2

Mô hình hướng đối tượng là vậy tuy nhiên bên dưới SQL không có kiểu dữ liệu nào tương ứng để biểu diễn cho address, buộc phải thêm các cột riêng lẽ cho từng thuộc tính trong address.

create table USERS (
   USERNAME varchar(15) not null primary key,
   ADDRESS_STREET varchar(255) not null,
   ADDRESS_ZIPCODE varchar(5) not null,
   ADDRESS_CITY varchar(255) not null
);

Như vậy chúng ta thấy được rằng thông tin của user được biểu diễn thông qua 2 class User và Address trong khi bảng User trong SQL được mô tả duy nhất trong một bảng chứa tất cả các thông tin của user và address, đây gọi là Granularity problem.

Subtype Problem – Thừa kế

Java là ngôn ngữ hướng đối tượng cung cấp cơ chế thừa kế được biểu diễn thông qua supperclass và subclass. Nhưng SQL thì lại không, sẽ không có chuyện 1 bảng thừa kế một bảng khác trong SQL. Vấn đề này được gọi là Subtype Problem.

Để làm rõ vấn đề hơn mình giả sử rằng trước đây ứng dụng của chúng ta chỉ cho phép thanh toán qua ngân hàng, giờ đây nó được mở rộng và cho phép khách hàng có thể thanh toán qua CreditCard. Để phát triển tính năng chúng ta sẽ sử dụng cơ thế thừa kế trong OOP với BillingDetails class giờ đây là một Abstract Class, CreditCard và BankAccount là các subclass.orm-resolve-problem-3

Có thể thấy rằng giờ đây thông tin thanh toán được biểu diễn dưới dạng thừa kế trong Java thông qua BillingDetails, CreditCard, BankAccount. Nhưng đối với SQL thì chúng ta không có cách nào mô phỏng giống như trong Java.

Thêm một điểm nữa chúng ta có thể chú ý đó là tại thời điểm runtime một User object có thể liên kết tới một CreditCard object hoặc BankAccount tuỳ thuộc vào logic của chương trình đây được gọi là đa hình trong OOP. Còn trong SQL, để xác định mối quan hệ giữa 2 đối tượng của 2 bảng khác nhau chúng ta phải sử dụng khoá ngoại để biểu diễn. Một khoá ngoại sẽ phải liên kết đến giá trị cố định trong bảng còn lại, vì vậy tính đa hình trong SQL cũng không được hỗ trợ.

Identity Problem – Định danh

Trong Java để xác định một object chúng ta có 2 cách để kiểm tra

  • Instance identity (tương đương với việc so sánh vùng nhớ) thực hiện bằng cách a == b.
  • Instance equality (so sánh giá trị các thuộc tính của object) thực hiện bằng cách triển khai equalshashCode method.

Trong SQL để định danh một dòng dữ liệu chúng ta phải thông qua giá trị của khoá chính. Như vậy trong 2 mô hình OOP và SQL có cách định danh khác nhau, đây được gọi là Identity Problem.

Một vấn đề khác liên quan đến định danh trong database đó là chọn khoá chính. Trong bảng User chúng ta đã thiết kế USERNAME là khoá chính, điều này vô tình khiến cho việc cập nhật USERNAME trở nên khó khăn vì không chỉ cập nhập USERNAME trong bảng User mà chúng ta cũng cần phải cập nhật USERNAME tương ứng trong bảng BILLINGDETAILS. Vấn đề này chúng ta sẽ tìm hiểu sâu hơn ở những bài viết sau, ở đây mình dưới thiệu sơ qua phương pháp surrogate key sử dụng khi chúng ta không tìm thấy được một thuộc tính đủ tốt để làm khoá chính.

Một surrogate key là một khoá chính có giá trị không liên về mặt ngữ nghĩa của bảng dữ liệu, nó đơn giản chỉ dùng để định danh 1 dòng dữ liệu trong bảng. Ví dụ chúng ta có thể tạo khoá chính là một số nguyên tự động tăng dần, khi đấy thay đổi USERNAME trở nên đơn giản hơn rất nhiều.

create table USERS (
    ID bigint not null primary key,
    USERNAME varchar(15) not null unique,
    ...
);
create table BILLINGDETAILS (
    ID bigint not null primary key,
    ACCOUNT varchar(15) not null,
    BANKNAME varchar(255) not null,
    USER_ID bigint not null,
    foreign key (USER_ID) references USERS
);

Association Problem – Quan hệ

Trong Java 2 thực thể có mối quan hệ với nhau được biểu diễn dưới dạng object reference còn trong CSDL quan hệ khoá ngoại sẽ đại diện cho mối quan hệ này, đây cũng là một quy tắc để đảm bảo tính Granularity (độ chi tiết) của thực thể. 

Object reference trong Java thường có 2 chiều trong đó mỗi object trong mối quan hệ đều chứa liên kết đến object kia.

public class User {
    Set billingDetails;
}

public class BillingDetails {
    User user;
}

Một lần nữa chúng ta có thể thấy cách biểu diễn mối quan hệ trong cả 2 mô hình lại một lần nữa khác nhau, nó được đặt tên là Association Problem.

Ngoài ra trong Java chúng ta có thể dễ dàng biểu diễn mối quan hệ many-to-many cách dễ dàng thông qua cách khai báo sau

public class User {
    Set billingDetails;
}

public class BillingDetails {
    Set users;
}

Trái ngược đối với SQL sẽ có chút phiền phức hơn khi chúng ta phải định nghĩa ra một bảng mới để biểu diễn mối quan hệ many-to-many.

create table USER_BILLINGDETAILS (
            USER_ID bigint,
            BILLINGDETAILS_ID bigint,
            primary key (USER_ID, BILLINGDETAILS_ID),
            foreign key (USER_ID) references USERS,
            foreign key (BILLINGDETAILS_ID) references BILLINGDETAILS
);

Data navigation – truy xuất dữ liệu

Việc truy xuất dữ liệu trong Java và CSDL quan hệ có một số điểm khác biệt cơ bản. Ví dụ như trong Java khi bạn muốn truy cập các BillingDetails của User thì có thể duyệt qua tất cả phần tử như sau:

for (BillingDetails bill : someUser.getBillingDetails()) {
     ... do something
}

Tuy nhiên đây không phải là cách hiệu quả khi muốn truy xuất dữ liệu trong SQL database. Để tăng tối ưu hiệu xuất của chương trình chúng ta cần giảm tối đa các câu truy vấn xuống database. Vì vậy khi muốn lấy thông tin của User kèm theo các BillingDetails thì chúng ta cần sử dụng phép join 2 giữa 2 bảng USERS và BILLINGDETAILS.

select * from USERS u
     left outer join BILLINGDETAILS bd on bd.USER_ID = u.ID
where u.ID = 123

Còn nếu chỉ muốn truy xuất các thông tin của User thôi thì chúng ta chỉ việc thực thi câu SQL đơn giản sau không cần join các bảng với nhau.

select * from USERS u where u.ID = 123

Tóm lược

Như vậy là chúng ta đã đi qua tất cả các vấn đề về sự không tương thích giữa 2 mô hình hướng đối tượng và dữ liệu quan hệ. Nắm được những vấn đề này, ở các phần sau chúng ta sẽ cùng nhau tìm hiểu chi tiết về cách ORM giải quyết chúng.

Nguồn tham khảo

Christian Bauer – Gavin King -Gary Gregory. Understanding object/relational persistence, Java Persistence with Hibernate, Second Edition, Manning.

https://livebook.manning.com/book/java-persistence-with-hibernate-second-edition/chapter-1/37

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x