Category: Backend

  • Testing SCD Loại 1 trong ETL validator

    Testing SCD Loại 1 trong ETL validator

    Xin chào mọi người, ở bài viết SCD (Slowly Changing Dimension) là gì? Các loại SCD và ví dụ cụ thể (https://magz.techover.io/2023/07/03/scd-slowly-changing-dimension-la-gi-cac-loai-scd-va-vi-du-cu-the/) chúng ta đã nắm được sơ lược về SCD type 1. Hôm nay chúng ta cùng đi tìm hiểu kỹ hơn về SCD type 1 và các test cases cần thiết trong quá trình kiểm thử nhé!

    1. Tìm hiểu về SCD type 1 (Ghi đè – Overwrite)

    Ở SCD1 các dữ liệu mới sẽ ghi đè dữ liệu cũ, không theo dõi dữ liệu lịch sử.

    Ví dụ:

    Original Record – Dữ liệu gốc

    Cust_ID Name City
    1001 Nguyễn Văn A HCM
    1002 Nguyễn Văn B Nam Định

    Updated Record – Dữ liệu được thay đổi

    Cust_ID Name City
    1001 Nguyễn Văn A Hà Nội
    1002 Nguyễn Văn B Nam Định

    Ưu điểm: Dễ bảo trì.

    Nhược điểm: Không kiểm tra được dữ liệu lịch sử.

    2. SCD1 validation test cases

    image

    Kiểm tra SCD Loại 1 khá đơn giản vì chúng ta có thể đạt được kết quả bằng cách so sánh đơn giản giữa dữ liệu nguồn và dữ liệu đích. Ví dụ chúng ta có bẳng nguồn và bảng đích như sau:

    Employee table – bảng nguồn với các cột:

    Cust_ID
    Name
    City

    EMPLOYEE_DIM SCD Type 1 – bảng đích với các cột dưới đây:

    Cust_ID
    Name
    City
    Partition_date

    2.1 Test case 1: Chúng ta sẽ đi kiểm tra số lượng cột, tên cột và data type từng cột có đúng với yêu cầu document không?

    VD: Cus_ID có type là bigint, thì trên bảng đích data type cũng là bigint.

    Target Query: Describe Table_Name; or Desc Table_Name

    2.2 Test case 2: Kiểm tra số lượng records.

    Source Query: Select count (*) from db.Emlpoyee

    Target Query: Select count (*) from db.Employee_Dim

    2.3 Test case 3: Kiểm tra logic insert

    1. Insert thêm records mới ở bảng nguồn:

    Chúng ta sẽ thêm các records mới tại bảng nguồn, rồi khởi chạy job ETL. Sau khi job chạy thành công thì kiểm tra dữ liệu ở bảng đích có lên đúng và đủ so với bảng nguồn không.

    1. Delete records ở bảng nguồn

    Ngược lại với thêm mới, chúng ta sẽ xóa bớt dữ liệu ở bảng nguồn rồi chạy job ETL. Sau khi job chạy thành công thì kiểm tra dữ liệu ở bảng đích và bảng nguồn có mapping với nhau không.

    2.4 Test case 4: Kiểm tra logic update

    Tại bảng nguồn chúng ta sẽ update các bản ghi trước đó, rồi chạy job ETL. Sau khi job chạy thành công, kiểm tra dữ liệu bảng đích có lên đúng so với bảng nguồn không.

    2.5 Test case 5: Kiểm tra dữ liệu có bị duplicate không? – Xác minh tính duy nhất của dữ liệu

    Target Query:

    Select Cust_ID, Name, City, Partition_date, count(*) from db.Employee_Dim group by Cust_ID, Name, City, Partition_date having count(*) > 1 ;

    Select Cust_ID, count(Cust_ID) from db.Employee_Dim group by Cust_ID having count(Cust_ID) > 1;

    2.6 Test case 6: Kiểm tra dữ liệu null

    Target Query:

    Select * from db.Employee_Dim where Cust_ID is not null;

    Kết luận

    • SCD1: các dữ liệu mới sẽ ghi đè dữ liệu cũ, không theo dõi dữ liệu lịch sử
    • Kiểm tra số lượng bản ghi bảng đích so với bảng nguồn
    • Sau khi thay đổi dữ liệu bảng nguồn (insert, delete, update) phải thực hiện quy trình ETL để EMPLOYEE_DIM có dữ liệu mới nhất
    • Sửa đổi một vài bản ghi trong bảng nguồn bằng cách cập nhật các giá trị trong các cột chính (key columns) để kiểm tra logic update
    • Xác minh tính duy nhất của dữ liệu
    • Xác minh rằng sự khác biệt là như mong đợi

    Trên đây là những chia sẻ của mình về testing SCD type 1 trong ETL testing, mong rằng bài viết này sẽ giúp ích được cho các bạn. Nếu mọi người có thắc mắc hay câu hỏi gì đừng ngần ngại comment và cùng nhau giải đáp nhé!

    Hẹn gặp lại mọi người trong bài viết tiếp theo.

  • SCD (Slowly Changing Dimension) là gì? Các loại SCD và ví dụ cụ thể

    SCD (Slowly Changing Dimension) là gì? Các loại SCD và ví dụ cụ thể

    Xin chào mọi người, ở bài viết trước mình đã chia sẻ về ETL testing và trong quá trình test ETL mình có nhắc đến việc chúng ta sẽ đi kiểm tra, theo dõi cách ghi/thay đổi dữ liệu trong bảng đích đã đúng yêu cầu hay chưa? Vậy cách ghi/thay đổi dữ liệu đó là gì nhỉ? Hôm nay chúng ta sẽ đi tìm hiểu về Slowly Changing Dimension (SCD) nhé!


    1. Slowly Changing Dimension (SCD) là gì?

    image

    SCD (Slowly Changing Dimension) có thể hiểu một cách đơn giản nhất đó là: so sánh dữ liệu nguồn với dữ liệu bảng đích hiện có bằng cách sử dụng Khóa nghiệp vụ – Business key (Khóa duy nhất – Unique Key).

    Nếu không có bản ghi nào khớp thì sẽ coi là Bản ghi mới hoặc Nếu bản ghi khớp thì sẽ so sánh các thuộc tính với các thuộc tính đã thay đổi nếu dữ liệu có vẻ được cập nhật thì nó cập nhật bản ghi hoặc nếu không thì nó để nguyên như không thay đổi.

    Slowly Changing Dimension sẽ kiểm tra các thuộc tính cho ba trường hợp: Bản ghi mới, đã thay đổi hoặc chưa thay đổi.

    2. Tại sao cần Slowly Changing Dimension?

    Trong thế giới Datawarehouse, đôi khi việc theo dõi sự thay đổi kích thước theo thời gian là rất quan trọng. Điều này giúp chúng ta theo dõi dữ liệu tốt hơn và cũng tạo ra các sản phẩm hiệu quả tùy thuộc vào các trường hợp sử dụng.

    3. Tại sao lại gọi là Slowly Changing Dimension?

    Slowly Chaning Dimension – gọi như trên có nghĩa là chúng ta sẽ phải sử dụng thành phần này chỉ cho các bảng không được cập nhật thường xuyên.

    Lưu ý: Không áp dụng cho bảng thường xuyên thay đổi, chỉ áp dụng trên các bảng kích thước (Dimension table) thay đổi chậm.

    4. Các loại SCD – Ví dụ về Slowly Changing Dimensions trong data warehouse (High-Level)

    Các loại SCD (High-Level)

    Sau đây là các loại SCD, mỗi loại đều có một số ưu điểm và nhược điểm riêng.

    1. Type – 0 Giữ lại dữ liệu gốc
    2. Type – 1 Ghi đè lên dữ liệu hiện có
    3. Type – 2 Thêm các bản ghi mới trên cùng một bảng
    4. Type – 3 Thêm cột mới trên cùng một bảng
    5. Type – 4 Sử dụng bảng lịch sử
    6. Type – 6 Phương pháp kết hợp (Loại 1 + Loại 2 + Loại 3)

    image

    SCD type 1 (Ghi đè)

    Loại hành động SCD đầu tiên có thể thực hiện được là ghi đè. Ở đây, các giá trị kích thước được ghi đè bởi các giá trị mới.

    Ví dụ: Khách hàng Nguyễn Văn A chuyển từ HCM đến Hà Nội, thì thành phố của anh ấy sẽ được cập nhật với giá trị mới nhất, tức là Hà Nội

    Original Record – Dữ liệu gốc

    Cust_ID Name City
    1001 Nguyễn Văn A HCM
    1002 Nguyễn Văn B Nam Định

    Updated Record – Dữ liệu được thay đổi

    Cust_ID Name City
    1001 Nguyễn Văn A Hà Nội
    1002 Nguyễn Văn B Nam Định

    Trong ví dụ trên, khách hàng đã di chuyển từ nơi này sang nơi khác và địa chỉ gần đây đã được ghi đè lên các bản ghi hiện có.

    Nhược điểm: chúng ta không thể truy xuất thông tin địa chỉ trước đây của anh ấy từ tình huống này.

    SCD type 2

    • Thêm bản ghi mới
    • Chúng ta có thể nắm bắt thay đổi thuộc tính bằng cách thêm một cột mới làm khóa thay thế (VD: IsActive)

    Khi giá trị của bản ghi hiện tại thay đổi, bản ghi hiện tại được đánh dấu là không hoạt động (inactive – 0) và bản ghi mới được insert vào.

    Kết quả, sẽ có 2 bản ghi được liên kết với Nguyễn Văn A trong bảng được cập nhật, nhưng chỉ có phiên bản mới nhất được đánh dấu là hoạt động (active – 1).

    Original Record – Dữ liệu gốc

    Cust_ID Name City IsActive
    1001 Nguyễn Văn A HCM 1
    1002 Nguyễn Văn B Nam Định 1

    Updated Record – Dữ liệu được thay đổi

    Cust_ID Name City IsActive
    1001 Nguyễn Văn A HCM 0
    1001 Nguyễn Văn A Hà Nội 1
    1002 Nguyễn Văn B Nam Định 1

    Ưu điểm: thỏa mãn điểm trừ trước đó theo dõi dữ liệu lịch sử bằng cách tạo mục nhập mới trên cùng một bảng.

    Nhược điểm: mặc dù nó nắm bắt dữ liệu lịch sử, nhưng nó có thể dẫn đến hoạt động tốn kém ở phía cơ sở dữ liệu.

    SCD Type 3 (Thêm cột giá trị trước đó)

    Loại SCD phổ biến thứ ba là thêm một cột giá trị trước đó. Ở đây, các phiên bản trước và hiện tại được duy trì trong một hàng.

    Hạn chế của phương pháp này là nó sẽ chỉ có hiện tại/trước đó chứ không phải toàn bộ lịch sử

    Original Record – Dữ liệu gốc

    Cust_ID Name City
    1001 Nguyễn Văn A HCM
    1002 Nguyễn Văn B Nam Định

    Updated Record – Dữ liệu được thay đổi

    Cust_ID Name Current City Previous City
    1001 Nguyễn Văn A Hà Nội HCM
    1002 Nguyễn Văn B Nam Định

    SCD Type 4: Thêm bảng mới (Bảng lịch sử)

    • Sử dụng bảng Lịch sử
    • Trong cách tiếp cận này, bảng lịch sử riêng biệt được tạo ra để theo dõi các thay đổi.
    • Bảng chính sẽ chỉ có dữ liệu mới nhất
    • Ưu điểm: Phản hồi nhanh hơn đối với các truy vấn yêu cầu dữ liệu mới nhất. Dễ quản lý và viết mã, thuận lợi cho các thuộc tính có tính biến động cao hoặc được sử dụng thường xuyên ở kích thước rất lớn.
    • Nhược điểm: Đôi khi tổng hợp/tham gia giữa dữ liệu hoạt động và lịch sử có thể mất thời gian và trở nên phức tạp

    Customer Table

    Cust_ID Name City
    1001 Nguyễn Văn A Hà Nội
    1002 Nguyễn Văn B Nam Định

    Customer History Table

    Cust_ID Name City Last_updated_date
    1001 Nguyễn Văn A HCM 11-03-2023
    1001 Nguyễn Văn A Hà Nam 11-05-2023
    1001 Nguyễn Văn A Bắc Ninh 11-06-2023

    SCD Type 6

    Phương pháp kết hợp (Loại 1 + Loại 2 + Loại 3)

    • Ưu điểm: Mọi bản chụp thay đổi dữ liệu đều có trong cùng một bảng
    • Nhược điểm: Phức tạp, khó quản lý, bảng lớn.
    Cust_ID Name City EffectiveFrom EffectiveTo IsActive
    1001 Nguyễn Văn A HCM 11-03-2023 11-05-2023 0
    1001 Nguyễn Văn A Hà Nam 11-05-2023 11-06-2023 0
    1001 Nguyễn Văn A Bắc Ninh 11-06-2023 03-07-2023 0
    1001 Nguyễn Văn A Hà Nội 03-07-2023 1
    1002 Nguyễn Văn B Nam Định 22-02-2023 1

    Ở ví dụ trên, dữ liệu lịch sử được theo dõi trong hàng mới được xác định bởi các trường ngày bắt đầu và ngày kết thúc. Ngoài ra, chúng ta có thể có trường cờ isActive để xác định bản ghi hiện tại từ danh sách. Chúng ta có thể duy trì lịch sử của tất cả các thay đổi đồng thời cập nhật giá trị hiện tại trên các bản ghi hiện có.


    Trên đây là những kiến thức về SCD (Slowly Changing Dimension), Các loại SCD và kịch bản kiểm thử, bài viết cũng khá dài rồi, nếu có cơ hội, bài viết tới mình sẽ chia sẻ chi tiết hơn về SCD type 2. Mong rằng bài viết này sẽ giúp ích được cho mọi người trong quá trình tìm hiểu về SCD. Nếu mọi người có thắc mắc hay câu hỏi gì đừng ngần ngại comment và cùng nhau giải đáp nhé!

    Hẹn gặp lại mọi người trong bài viết tiếp theo.

    Tài liệu tham khảo:

    https://www.expressanalytics.com/blog/what-is-a-slowly-changing-dimension-and-the-logic-in-implementation/

    https://www.learnmsbitutorials.net/slowly-changing-dimensions-ssis.php

  • Tìm hiểu về ETL Testing, ETL (Extract, Transform, and Load) Process

    Tìm hiểu về ETL Testing, ETL (Extract, Transform, and Load) Process

    Xin chào mọi người, chắc hẳn các bạn đã từng nghe qua thuật ngữ ETL và tự hỏi rằng ETL là gì? Đối với tester muốn test ETL cần phải làm gì? Quy trình ETL ra sao? Tại sao phải ETL nhỉ? Và test ETL khác gì so với test app, test web? Trước đây, mình cũng thế, đặt ra hàng vạn câu hỏi vì sao rồi đi tìm hiểu, sau quá trình học và có chút kinh nghiệm thực tế, hôm nay mình xin giới thiệu đôi chút kiến thức về ETL testing, tổng quan về quy trình ETL và test ETL khác gì so với test app, web.

    1. Khái niệm

    1.1. ETL Thử nghiệm/Kiểm thử ETL là gì?

    Kiểm thử ETL là quá trình kiểm thử được thực hiện để đảm bảo dữ liệu được tải từ nguồn đến đích sau khi chuyển đổi là chính xác, là việc xác minh dữ liệu ở các giai đoạn trung gian đang được sử dụng giữa nguồn và đích. ETL là từ viết tắt của Extract-Transform-Load.

    1.2. Kiểm tra kho dữ liệu (data warehouse) là gì?

    Kiểm tra kho dữ liệu là một phương pháp kiểm tra trong đó dữ liệu bên trong kho dữ liệu được kiểm tra tính toàn vẹn, độ tin cậy, độ chính xác và tính nhất quán để tuân thủ khung dữ liệu của công ty. Mục đích chính của thử nghiệm kho dữ liệu là để đảm bảo rằng dữ liệu được tích hợp bên trong kho dữ liệu đủ tin cậy để một công ty đưa ra quyết định.

    1.3. ETL là gì? ETL hoạt động như nào?

    ETL là viết tắt của Extract-Transform-Load, là một quy trình trích xuất dữ liệu từ các hệ thống nguồn khác nhau, sau đó chuyển đổi dữ liệu (như áp dụng phép tính, phép nối, v.v.) Và cuối cùng tải dữ liệu vào hệ thống Kho dữ liệu. Trích xuất, chuyển đổi và tải (ETL) hoạt động bằng cách di chuyển dữ liệu từ hệ thống gốc đến hệ thống đích trong các chu kỳ định kỳ. Quy trình ETL hoạt động theo ba bước:

    • Extract: Trích xuất dữ liệu có liên quan từ cơ sở dữ liệu nguồn
    • Transform: Chuyển đổi dữ liệu để phù hợp hơn cho việc phân tích
    • Load: Tải dữ liệu vào cơ sở dữ liệu đích

    image

    1.4 Tại sao chúng ta phải ETL dữ liệu?

    Nếu chúng ta vẫn để nguyên các dữ liệu trên các database của các dữ liệu nguồn, chúng ta vẫn làm được các báo cáo phân tích, … Vậy tại sao chúng ta phải ETL dữ liệu làm gì?

    Như đã nói trên, bạn dùng ETL dữ liệu để chuyển mục đích, và tối ưu hóa mục đích sử dụng dữ liệu của các phần mềm từ ghi nhận các nghiệp vụ phát sinh hàng ngày, sang mục đích khai thác, vận hành, và phân tích các dữ liệu này để các nhà quản trị tìm ra các cơ may phát triển, các hoạt động kinh doanh mới đề vận hành doanh nghiệp – và đây chính là mục đích của ETL, và là nguyên nhân bạn cần công cụ này – chuyển đổi công năng sử dụng dữ liệu để cung cấp cho nhà quản trị.

    2. Quy trình kiểm thử ETL

    Tương tự như các Quy trình kiểm thử khác, ETL cũng trả qua các giai đoạn khác nhau. Các giai đoạn của quá trình kiểm thử ETL như sau:

    image

    3. ETL Tools

    Trên thị trường, có rất nhiều tools ETL, nhưng dưới đây là vài tool nổi bật nhất mọi người hay dùng:

    • Marklogic: https://www.marklogic.com/product/getting-started/
    • Oracle: https://www.oracle.com/index.html
    • Amazon redshift: https://aws.amazon.com/redshift/?Nc2=h_m1

    4. Mình đã sử dụng AWS trong ETL testing như thế nào?

    Như ở trên, chúng ta đã hiểu, ETL testing là kiểm tra để đảm bảo dữ liệu được tải từ nguồn đến đích sau khi chuyển đổi là chính xác. Lý thuyết là vậy, còn thực hành sẽ như nào nhỉ?

    Thật khó để mình có thể chia sẻ hết kinh nghiệm trong quá trình tìm hiểu, học và kiểm thử ETL trong bài viết này, nhưng mình sẽ lấy 1 Ví dụ để mô tả một cách dễ hiểu nhất những gì 1 tester cần làm trong quá trình kiểm thử ETL. Từ đó, các bạn dễ hình dung, hiểu hơn về ETL testing và có thể áp dụng trong tương lai.

    Ví dụ: Dưới đây là luồng di chuyển dữ liệu từ hệ thống nguồn (Stream data source) đến hệ thống đích (S3 Data Lake Target) trên AWS. Tester sẽ cần làm gì để test dữ liệu từ Source lên S3?

    image

    • B1. Bạn sẽ cần chuẩn bị dữ liệu thô (VD: file csv, file parquet, …) để up lên source.
    • B2. Vậy làm cách nào để cho data chạy từ source lên hệ thống đích được nhỉ? Bạn sẽ cần phải khởi chạy ETL job.
    • B3. Sau khi chạy, chúng ta sẽ kiểm tra job ETL đã chạy thành công chưa?
    • B4. Sau khi job chạy thành công, kiểm tra hệ thống đích (VD: S3 Data Lake Target) có tạo bảng như mong đợi?
    • B5. Kiểm tra dữ liệu bảng đích. VD: Số lượng bản ghi, số lượng column, tên column, data type từng bản ghi, data trong từng column, … Kiểm tra, theo dõi cách ghi/thay đổi dữ liệu trong bảng đích đã đúng yêu cầu hay chưa.

    5. ETL testing giống và khác gì so với test mobile, test web?

    5.1. Giống nhau:

    • Trước hết, để test bất kỳ cái gì chúng ta đều phải đọc và hiểu tài liệu đặc tả. Lên kế hoạch kiểm thử và estimate thời gian kiểm thử.
    • Thiết kế test case, đảm bảo test đủ các trường hợp có thể xảy ra.
    • Chuẩn bị data test, môi trường test, …
    • Mục đích cuối cùng đều là đảm bảo chất lượng sản phẩm, đảm bảo đầu ra đúng với nhu cầu khách hàng.
    • ….

    5.2. Khác nhau:

    Vậy ETL testing có gì khác biệt so với test web và mobile? Dưới đây là một vài điểm khác biệt mà mình thấy được trong quá trình làm việc với ETL:

    • Test app, web để kiểm tra giao diện (UI), tương tác và trải nghiệm người dùng (UX) hay các chức năng, giá trị hiển thị, … thì chúng ta sẽ cần so sánh đúng với yêu cầu đặc tả (SRS)/mong muốn của khách hàng. Tức là chúng ta đã có sẵn yêu cầu đầu ra, việc cần làm là kiểm tra tính đúng đắn so với yêu cầu đó.
    • Test ETL thì chúng ta cần có kiến thức về SQL. Vì thực tế luôn có những chuyển đổi dữ liệu (transform) phức tạp, hoặc transform data từ nhiều nguồn, nhiều khoảng thời gian, … nên để tìm ra được output expect (kết quả đầu ra) là điều không dễ dàng. Do đó, chúng ta cần viết script SQL chuẩn, đúng với tài liệu để có được kết quả đầu ra, từ đó mới có thể so sánh và kiểm tra dữ liệu.

    Kết luận

    • ETL là viết tắt của Trích xuất, Chuyển đổi và Tải (Extract, Transform and Load)

    • ETL cung cấp phương pháp di chuyển dữ liệu từ nhiều nguồn khác nhau vào kho dữ liệu.

    • Trong bước trích xuất đầu tiên, dữ liệu được trích xuất từ hệ thống nguồn vào khu vực tổ chức.

    • Trong bước chuyển đổi, dữ liệu được trích xuất từ nguồn được làm sạch và chuyển đổi.

    • Tải dữ liệu vào kho dữ liệu đích là bước cuối cùng của quy trình ETL.

    Trên đây là những chia sẻ của mình về ETL testing, mong rằng bài viết này sẽ giúp ích được cho các bạn. Nếu mọi người có thắc mắc hay câu hỏi gì đừng ngần ngại comment và cùng nhau giải đáp nhé!

    Hẹn gặp lại mọi người trong bài viết tiếp theo.

    Tác giả bài viết

    HanhTM2

    Tài liệu tham khảo:

    Https://www.guru99.com/etl-extract-load-process.html

    Https://aws.amazon.com/vi/what-is/etl/

  • Cách Grab sử dụng DynamoDB để xử lý hàng triệu đơn hàng mỗi ngày

    Cách Grab sử dụng DynamoDB để xử lý hàng triệu đơn hàng mỗi ngày

    Nội dung

    Giới thiệu

    Trong thực tế, sau khi một khách hàng đặt một đơn hàng GrabFood từ ứng dụng Grab, đối tác bán hàng sẽ chuẩn bị đơn hàng. Một đối tác tài xế (Grab Bike) sẽ tiếp nhận đơn hàng và vận chuyển cho khách hàng. Làm cách nào mà nền tảng đặt hàng (Order Platform) có thể xử lý hàng triệu đơn hàng như vậy mỗi ngày !?

    Nhìn chung, có thể phân loại các truy vấn mà Order Platform cần xử lý làm 2 loại: transactional queries vs analytical queries. Các truy vấn transactional ví dụ như: Create đơn hàng, Update đơn hàng, Get đơn hàng theo ID, Get các đơn hàng đang được xử lý của một khách hàng,… Những truy vấn này là những truy vấn quan trọng cần xử lý chính xác, hiệu quả. Các truy vấn analytical ví dụ như: Get lịch sử đơn hàng, Get các metrics thống kê về đơn hàng,…

    → Order Platform cần được thiết kế để có thể xử lý một lượng lớn dữ liệu transaction hàng tháng.

    Sau khi phân loại các mẫu truy vấn như trên, nhóm Kỹ sư của Grab đã đặt ra 3 mục tiêu thiết kế hệ thống Database để lưu trữ và xử lý như sau:

    • Stability (Tính ổn định): Giải pháp cơ sở dữ liệu phải đảm bảo phục vụ được khả năng đọc/ghi với thông lượng cao (Queries per Second, hay QPS). Xử lý đơn hàng trực tuyến phải có tính sẵn sàng cao.
    • Scalability and cost (Khả năng mở rộng và chi phí): Giải pháp phải đáp ứng được sự phát triển nhanh chóng về mặt business, ngoài ra giải pháp phải hiệu quả về chi phí trên quy mô lớn.
    • Consistency (Tính nhất quán): strong consistency cho các đối với các transactional querieseventually consistency với các analytical queries.

    Với các mục tiêu đó, Grab team đã quyết định thiết kế giải pháp cơ sở dữ liệu sử dụng các loại Database khác nhau, một để xử lý các transactional queries (OLTP) và một để xử lý các analytical queries (OLAP).

    Hình 1: Tổng quan giải pháp cơ sở dữ liệu cho Order Platform

    Vì các mục đích sử dụng khác nhau nên các cơ sở dữ liệu OLTP và OLAP sẽ lưu trữ data theo các cách khác nhau, với cùng một nguồn data nhưng OLTP sẽ lưu trữ chúng trong thời gian ngắn, ngược lại thì OLAP sẽ lưu trữ data trong thời gian dài hơn nhằm truy xuất các thông tin lịch sử và thống kê với dữ liệu. Trong Hình 1, Grab team sử dụng một Data Ingestion pipeline để có thể lưu trữ giữ liệu nhất quán trong cả 2 cơ sở dữ liệu. Cần lưu ý rằng, pipeline này sẽ hoạt động bằng cách sử dụng các phương pháp xử lý asynchronous (bất đồng bộ) vì chúng ta không cần phải xử lý các truy vấn với OLAP trực tuyến, real-time như OLTP.

    Trong bài viết này, tôi sẽ tập trung vào phân tích cách Grab xử lý hàng triệu đơn hàng mỗi ngày, chính vì vậy mà trong các phần tiếp theo, tôi sẽ không đề cập đến cơ sở dữ liệu OLAP. Các bạn nếu có hứng thú, có thể tìm đọc thêm các tài liệu từ Grab Tech Blog.

    Với các truy vấn OLTP, Grab team chọn DynamoDB là công nghệ lưu trữ và xử lý dữ liệu. Trong phần 2, tôi sẽ phân tích tại sao DynamoDB lại phù hợp. Phần 3, tôi sẽ phân tích Grab sử dụng DynamoDB như thế nào trong thiết kế mô hình dữ liệu hiệu quả.

    Tại sao lại là DynamoDB?

    Để trả lời câu hỏi này, tôi sẽ phân tích tính phù hợp của DynamoDB đối với việc đáp ứng các mục tiêu thiết kế được nêu ở phần trước.

    Điều đầu tiên để nói về DynamoDB, nó là một cơ sở dữ liệu NoSQL, fully-managed được cung cấp bởi AWS, có tính khả dụng cao (high availability), hiệu năng cao (high performance) với hiệu suất chỉ 1 phần nghìn giây (single-digit millisecond performance), khả năng mở rộng quy mô vô hạn mà không giảm hiệu suất (infinite scaling with no performance degradation) → Stability.

    High availability

    DynamoDB tự động phân phối dữ liệu và lưu lượng truy cập qua một số lượng máy chủ vừa đủ để xử lý các yêu cầu lưu trữ và thông lượng trong khi vẫn đảm bảo hiệu suất nhanh chóng và nhất quán. Tất cả dữ liệu được lưu trữ sử dụng ổ đĩa SSD và tự động được replicated qua các AZs khác nhau → high availability & high durability. Hơn nữa, chúng ta có thể sử dụng Global Tables để đồng bộ dữ liệu qua các Regions.

    Data models

    Trước khi nói về hiệu suất, tôi đề cập một chút về các mô hình dữ liệu mà DynamoDB hỗ trợ. Trong DynamoDB, có 2 mô hình dữ liệu đó là key-value store (sử dụng hash table) → cho phép hiệu suất truy vấn O(1) và wide-column store (sử dụng B-tree) → cho phép hiệu suất truy vấn O(log(n)), n là kích thước của 1 collections các phần tử có chung partition key.

    DynamoDB cho phép truy cập dữ liệu theo nhiều cách (access patterns) khác nhau với hiệu suất cao → điều quan trọng với DynamoDB đó là chúng ta phải thiết kế mô hình dữ liệu hiệu quả, tôi sẽ tiếp tục đề cập đến điều này trong phần 3 – Grab đã sử dụng DynamoDB thế nào?

    Infinite scaling with no performance degradation

    DynamoDB có khả năng scale vô hạn là nhờ cơ chế sharding dữ liệu qua nhiều server instances. Partitions là đơn vị lưu trữ cốt lõi của các DynamoDB tables. Khi một HTTP request (ứng dụng giao tiếp với DynamoDB qua HTTP connection thay vì TCP connection như các cơ sở dữ liệu truyền thống khác) tới DynamoDB, request router sẽ lấy partition key trong request đó và áp dụng một hàm băm (hash function) đối với partition key này. Kết quả của hàm băm sẽ chỉ định server nơi mà dữ liệu được lưu trữ, request sau đó sẽ được chuyển tiếp đến server nơi lưu trữ dữ liệu để thực hiện read/write với dữ liệu. Thiết kế này giúp cho DynamoDB có thể bổ sung thêm các storage nodes vô hạn khi dữ liệu scale up.

    Trong các phiên bản sớm hơn của DynamoDB, thông lượng được chia sẻ đều giữa các partitions → có thể dẫn đến mất cân bằng truy cập dữ liệu (có partition thì cần truy cập nhiều → không đủ tài nguyên, có partition thì nhu cầu truy cập ít hơn → thừa tài nguyên). Tuy nhiên, DynamoDB team hiện nay đã cho phép adaptive capacity, có nghĩa là thông lượng sẽ tự động được đáp ứng đủ cho các items cần truy cập nhiều, DynamoDB tự động cung cấp capacity cao hơn cho các partitions có nhiều lưu lượng truy cập hơn → DynamoDB có thể đáp ứng thông lượng lên đến 1000 WCUs (write capacity units) và 3000 RCUs (read capacity units). Hay nói một cách khác, DynamoDB có thể đáp ứng đến tối đa 3000 read Queries per second1000 write Queries per Second → hiệu suất cao.

    Ngoài ra, AWS còn cung cấp một tính năng nữa cho DynamoDB, gọi là DynamoDB Accelerator (DAX) – một fully-managed in-memory cache cho bảng DynamoDB. Mặc dù hiệu suất của DAX có thể không thực sự mạnh như Redis nhưng DAX có thể là một giải pháp caching phù hợp cho phép tốc độ đủ nhanh, ít bảo trì và tiết kiệm chi phí hơn so với các bộ đệm khác.

    Tóm lại, với DynamoDB, chúng ta vẫn có thể đảm bảo single-digit millisecond performance khi dữ liệu scale up. Chúng ta có thể mở rộng dữ liệu lên đến 10TB mà vẫn không suy giảm hiệu suất như sử dụng với 10GB. Điều mà không thể được đáp ứng trong các cơ sở dữ liệu quan hệ – ở đó, hiệu suất giảm dần khi dữ liệu scale up.

    Consistency

    Khi nói về các databases và các hệ thống phân tán, một trong những thuộc tính cần xem xết đó là các cơ chế consistency mà nó chúng hỗ trợ.

    Với DynamoDB, có 2 tùy chọn consistency mà cơ sở dữ liệu này hỗ trợ:

    • Strong consistency
    • Eventual consistency

    Với strong consistency, bất kỳ item nào chúng ta đọc từ DynamoDB sẽ phản ánh tất cả các yêu cầu writes đã xảy ra trước đó, trước khi yêu cầu đọc được thực thi. Ngược lại, với eventual consistency thì các yêu cầu đọc items có thể không phản ánh tất cả các yêu cầu writes xảy ra trước đó.

    → Các transactional queries trong Order Platform hoàn toàn có thể được sử dụng với strong consistency trong DynamoDB.

    Cost effective

    DynamoDB cung cấp một mô hình định giá linh hoạt. Với hầu hết các cơ sở dữ liệu, chi phí dựa trên 1 lượng capacities nhất định cho máy chủ bao gồm CPU, RAM, Hard Disks,… → điều này không tối ưu vì chúng ta sẽ phải trả tiền cho tài nguyên thay vì trả cho workload ta sử dụng (bao nhiêu truy vấn mỗi giây/queries per second)

    Ngược lại, DynamoDB được định giá trực tiếp dựa trên workload mà ứng dụng cần, chúng ta chỉ định thông lượng dựa trên WCU (write capacity units) và RCU (read capacity units). Một RCU cung cấp 1 strongly-consistent read per second hoặc 2 eventually-consistent reads per second, lên đến 4KB kích thước. Một WCU cho phép 1 write per second, lên đến 1KB kích thước → Chi phí cho Read và Write là riêng biệt → Có thể tối ưu 1 trong 2 độc lập dựa trên nhu cầu ứng dụng.

    Điều thú vị với DynamoDB là chúng ta có thể điều chỉnh thông lượng WCU và RCU nếu cần. Ví dụ như vào ban đêm hoặc cuối tuần, chúng ta có thể giảm thông lượng để tiết kiệm chi phí.

    Nếu chúng ta không estimate được thông lượng của ứng dụng → DynamoDB cung cấp tùy chọn On-Demand capacity mode (chi phí cao hơn Provisioned capacity mode) → Có thể sử dụng On-Demand capacity mode trong quá trình phát triển, thực thi load testing để đưa ra Provisioned capacity phù hợp → tiết kiệm chi phí.

    Năm 2017, AWS công bố một tính năng gọi là Time-to-live (TTL) cho DynamoDB, cho phép DynamoDB tự động xóa các item khi chúng ta chỉ muốn lưu trữ chúng cho 1 thời gian ngắn → điều này phù hợp với nhu cầu sử dụng DynamoDB của Grab, khi mà Grab team đã thiết kế một Data Ingestion Pipeline để lưu trữ dữ liệu lâu dài trong cơ sở dữ liệu OLAP. Việc lưu trữ dữ liệu trong DynamoDB trong thời gian ngắn còn cho phép hiệu quả chi phí cũng như đảm bảo hiệu suất luôn ổn định cao → đáp ứng được các transactional queries hàng ngày.

    Sử dụng DynamoDB như thế nào?

    Single-table design

    Khi mô hình hóa dữ liệu với DynamoDB, chúng ta nên sử dụng càng ít bảng càng tốt, lý tưởng nhất là chỉ sử dụng 1 bảng cho toàn bộ ứng dụng → single-table design. Tại sao lại như vậy !?

    Với các cơ sở dữ liệu quan hệ, thông thường chúng ta sẽ tạo 1 bảng ứng với mỗi entity (thực thể) trong ứng dụng. Các bảng có thể liên kết với nhau thông qua foreign keys → qua đó dữ liệu có thể được truy vấn qua các bảng sử dụng các phép toán joins. Các phép joins này khá tốn kém, khi mà nó phải scan nhiều bảng, so sánh các giá trị,… và trả về các kết quả.

    DynamoDB được xây dựng cho các use cases với hiệu suất truy vấn cao, quy mô dữ liệu lớn, như Order Platform của Grab → Các phép toán joinskhông phù hợp với các use cases như vậy. Thay vì cải thiện các phép toán joins trên quy mô dữ liệu lớn, DynamoDB giải quyết vấn đề bằng cách loại bỏ hoàn toàn việc sử dụng joins trong truy vấn. Loại bỏ bằng cách nào !?

    Cơ chế pre-join trong DynamoDB có thể cho phép liên kết dữ liệu tương tự như các cơ sở dữ liệu quan hệ. Cơ chế này sử dụng các item collections. Một item collection là tập các items trong bảng chia sẻ chung partition key. Như vậy, nhờ pre-join mà DynamoDB cho phép thiết kế cơ sở dữ liệu ứng dụng chỉ với 1 bảng. Tất nhiên, có những hạn chế nhất định với single-table design, tuy nhiên trong phạm vi bài viết này tôi sẽ không đi sâu vào phân tích những hạn chế đó. Trong các phần sau, tôi muốn làm nổi bật thiết kế của Grab sử dụng single-table design như thế nào để xử lý các transactional queries với các đơn hàng.

    Data modeling

    Trong DynamoDB, khi thiết kế mô hình dữ liệu, chúng ta phải xác định Primary keys của bảng. DynamoDB cung cấp 2 loại Primary keys khác nhau:

    • Simple primary keys: chỉ bao gồm một thuộc tính, gọi là partition key.
    • Composite primary keys: là sự kết hợp của partition key và một thuộc tính khác, gọi là sort key. Đôi khi, chúng ta có thể gọi partition key là hash key, sort key là range key.

    Để thiết kế cơ sở dữ liệu hiệu quả, trước hết chúng ta cần hình dung về mô hình dữ liệu của Order Platform. Order Platform cần thực hiện các truy vấn với đơn hàng (order), các đơn hàng liên quan đến khách hàng hay người đặt hàng, mỗi đơn hàng sẽ có 1 trong 3 trạng thái: ongoing, completed, hoặc canceled → Khá đơn giản và phù hợp với single-table design trong DynamoDB. Hình dưới đây cho thấy thiết kế này của Grab.

    Hình 2: Minh họa bảng DynamoDB lưu trữ các đơn hàng của Order Platform

    Trong hình 2 ở trên, Grab team đã đưa ra một minh họa về bảng DynamoDB nơi mà lưu trữ các đơn hàng, với Partition keymã đơn hàng (order_id) (Lưu ý rằng hình trên chỉ mang tính chất minh họa, không phải là toàn bộ thiết kế của Order Platform).

    Thiết kế này cho phép các transactional queries với đơn hàng như Get, Create, Update được thực hiện với hiệu suất cao, strong consistency. Bởi cấu trúc dữ liệu sử dụng hash table → các truy vấn sử dụng order_id sẽ có độ phức tạp thời gian là O(1).

    Với các truy vấn liên quan đến 1 đơn hàng cụ thể, chúng ta có thể sử dụng order_id (partition key). Vậy với các truy vấn liên quan đến 1 khách hàng, ví dụ như lấy danh sách các đơn hàng ở trạng thái ongoing của khách hàng Alice như trong Hình 2, chúng ta có thể tiếp tục sử dụng mô hình dữ liệu trên không !?

    → Câu trả lời là có. Đến đây, chúng ta phải sử dụng một tính năng hữu ích khác được cung cấp bởi DynamoDB, đó là Global secondary index.

    Secondary indexes

    Primary keys có thể giới hạn các access patterns, như trong trường hợp của Grab, khi sử dụng primary keys với partition keyorder_id thì chúng ta không thể tiếp tục truy vấn với 1 khách hàng cụ thể trong bảng đơn hàng. Để giải quyết hạn chế này, DynamoDB đưa ra một khái niệm gọi là secondary indexes. Secondary indexes cho phép chúng ta re-shape lại dữ liệu sang 1 cấu trúc khác để phục vụ các truy vấn với access pattern khác so với thiết kế ban đầu.

    Khi tạo secondary index, chúng ta cần chỉ định key schema của index, key schema này tương tự như Primary keys của bảng, có thể chỉ cần partition key hoặc kết hợp partition key và sort key. Có 2 loại secondary indexes trong DynamoDB:

    • Local secondary indexes (LSI)
    • Global secondary indexes (GSI)

    Một LSI sử dụng chung partition key như primary key của bảng nhưng với một sort key khác đi → điều này cho phép các truy vấn theo range khác với access pattern ban đầu.

    Ngược lại, với GSI, chúng ta có thể chọn bất cứ thuộc tính nào làm partition key và sort key.

    → Trong trường hợp của Grab, với thiết kế ban đầu như Hình 2, bởi LSI không thay đổi partition key → không phù hợp với các truy vấn theo 1 khách hàng cụ thể → Grab team đã sử dụng GSI để giải quyết vấn đề này.

    Ví dụ với một truy vấn Get tất cả các đơn hàng trong trạng thái Ongoing của khách hàng có pax_idAlice. Nếu chúng ta sử dụng GSI chỉ với partition keypax_id thì việc truy vấn dựa trên pax_id (trường hợp này là Alice) sẽ trả về tất cả các đơn hàng của Alice → Filter trên các kết quả đó để trích xuất ra các đơn hàng của Alice có trạng thái Ongoing. Như vậy, nếu như Alice có nhiều đơn hàng thì việc Filter như vậy sẽ ảnh hưởng đến độ trễ truy vấn nói riêng và hiệu năng tổng thể của hệ thống nói chung. Giải quyết bằng cách nào !?

    → Sử dụng Sparse indexes

    Hình 3: GSI cho bảng đơn hàng

    GSI được Grab team thiết kế sử dụng ID của khách hàng (pax_id_gsi) làm partition key và thời gian tạo đơn hàng (created_at) làm sort key, sparse index cho phép tại bất kỳ thời điểm nào, bảng GSI chỉ lưu trữ các đơn hàng với trạng thái Ongoing (Khi một đơn hàng chuyển từ Ongoing → Completed thì nó sẽ tự động bị xóa khỏi GSI) → Cho phép độ trễ truy vấn thấp, hiệu suất tốt hơn và hiệu quả chi phí.

    Một vài lưu ý khi sử dụng GSI với DynamoDB:

    • Với GSI, chúng ta cần provision thêm throughput. RCU và WCU được sử dụng riêng biệt so với bảng ban đầu.
    • Dữ liệu được replicated từ bảng ban đầu sang GSI theo cơ chế asynchronous → chỉ cho phép eventual consistency.

    Time to live (TTL)

    Vì mục đích chỉ lưu trữ giữ liệu trong DynamoDB trong một thời gian ngắn (chúng ta có thể thấy điều này trên ứng dụng Grab Food, các đơn hàng sau một thời gian ~ 3 tháng sẽ không còn thấy trong lịch sử nữa :v) → Grab team sử dụng tính năng TTL được cung cấp bởi DynamoDB → cho phép tự động xóa các items khi expired. Để sử dụng TTL hiệu quả, Grab team chỉ thêm TTL với các items mới được add vào bảng, xóa các items không có thuộc tính TTL theo cách thủ công và chạy các script để xóa các items có TTL đã out of date khá lâu → cho phép tính năng TTL trên bảng với kích thước đủ nhỏ → hiệu quả khi DynamoDB tự động thực hiện scan bảng và xóa các items đã expired.

    Data ingestion pipeline

    Hình 4: Data ingestion pipeline

    Grab team sử dụng Kafka để đồng bộ dữ liệu giữa 2 cơ sở dữ liệu OLTP và OLAP. Sử dụng Kafka như thế nào, các bạn có thể đọc thêm từ Grab tech blog như trong tài liệu tham khảo [1]. Trong phạm vi bài viết này, tôi không phân tích về lựa chọn này. Tuy nhiên, nếu trong tương lai có thể, tôi muốn phân tích về tính khả thi của giải pháp sử dụng DynamoDB Streams để xử lý và đồng bộ dữ liệu giữa các cơ sở dữ liệu OLTP và OLAP.

    Kết luận

    Trong bài viết này, tôi đã dựa trên một case study là Order Platform của Grab Food để phân tích một số tính năng, nguyên lý của DynamoDB phù hợp với các ứng dụng cần OLTP. DynamoDB đã cho phép Order Platform đạt được tính ổn định, tính khả dụng cao, hiệu suất cao cùng với khả năng mở rộng quy mô vô hạn. Ngoài ra, tôi cũng giới thiệu các cơ chế để cải thiện hiệu suất, chi phí hiệu quả mà Grab team sử dụng như GSI, TTL. Data ingestion pipeline được giới thiệu như là 1 biện pháp mà Grab sử dụng để đồng bộ dữ liệu sang cơ sở dữ liệu OLAP phục vụ các truy vấn thống kê cũng như đảm bảo khả năng mở rộng nghiệp vụ ứng dụng về sau.

    Tài liệu tham khảo

    [1] Xi Chen, Siliang Cao, “How we store and process millions of orders daily”, https://engineering.grab.com/how-we-store-millions-orders, Accessed: 2022-03-31

    [2] Alex DeBrie, “The DynamoDB Book”

    [3] Amazon DynamoDB, https://disaster-recovery.workshop.aws/en/services/databases/dynamodb.html, Accessed: 2022-03-31

  • Bài toán tìm kiếm tương tự

    Bài toán tìm kiếm tương tự

    Danh mục nội dung

    Tìm kiếm KNN

    Thuật toán K-hàng xóm gần nhất, còn được gọi là KNN hoặc k-NN, là một thuật toán học máy có giám sát, không có tham số, được dùng trong phân loại dữ liệu. Thuật toán này sử dụng khoảng cách để phân loại hoặc dự đoán về một nhóm điểm dữ liệu, dựa trên giả định rằng các điểm dữ liệu tương tự sẽ gần nhau về khoảng cách.

    Ngày nay, tìm kiếm hàng xóm gần nhất đã trở thành một chủ đề nghiên cứu nóng bởi tính ứng dụng thực tiễn của nó, nhằm giúp người dùng có thể dễ dàng tìm thấy thông tin họ đang tìm kiếm trong thời gian hợp lý. Thuật toán k-NN là một trong những kỹ thuật giúp tìm chính xác các hàng xóm gần nhất, bởi nó so sánh khoảng cách của mỗi điểm dữ liệu với mọi điểm dữ liệu khác, vì vậy nó yêu cầu thời gian truy vấn tuyến tính (kích thước tập dữ liệu). Nhưng thật không may, hầu hết các ứng dụng hiện đại ngày nay đều có tập dữ liệu khổng lồ (hàng triệu) với chiều cao (hàng trăm hoặc hàng nghìn), vì vậy mà tìm kiếm tuyến tính sẽ tốn kém thời gian. Hãy thử tưởng tượng một thị trường C2C trong thế giới thực với hàng triệu sản phẩm có trong cơ sở dữ liệu và có thể có hàng nghìn sản phẩm mới được tải lên mỗi ngày. So sánh từng sản phẩm với tất cả hàng triệu sản phẩm là lãng phí và mất quá nhiều thời gian, có thể nói giải pháp này là không thể mở rộng. Ngoài ra, các ứng dụng hiện đại còn có nhiều ràng buộc bổ sung khác như mức tiêu thụ bộ nhớ hợp lý và/hoặc độ trễ thấp.

    Điều quan trọng cần lưu ý là mặc dù đã có rất nhiều tiến bộ gần đây về chủ đề này, nhưng k-NN vẫn là phương pháp khả dụng duy nhất để đảm bảo truy xuất chính xác hàng xóm gần nhất.

    Tìm kiếm ANN

    Để giải quyết những vấn đề của k-NN, một lớp các thuật toán mới ra đời có tên là ANN (Approximate Nearest Neighbors). Các thuật toán ANN đánh đổi độ chính xác để mang lại hiệu quả tìm kiếm nhanh chóng, trong thời gian chấp nhận được. Trong một thị trường C2C thực tế, nơi mà số lượng hàng xóm thực tế cao hơn K hàng xóm gần nhất cần tìm kiếm rất nhiều, ANN có thể cho phép đạt được độ chính xác đáng kể khi so sánh với KNN, trong một thời gian ngắn.

    Trong các ứng dụng hiện đại, sai số nhỏ về độ chính xác đổi lại với độ trễ thấp mang lại nhiều lợi ích cho người dùng. Hai ví dụ dưới đây cho thấy điều đó:

    • Tìm kiếm trực quan – Là một người dùng, nếu tôi đang muốn tìm kiếm một bức ảnh về chiếc giày yêu thích, tôi sẽ không bận tâm đến thứ tự xuất hiện của các kết quả trả về, tôi có thể thỏa mãn nhu cầu tìm kiếm của mình nếu như một số ít kết quả mong muốn được hiển thị gần nhất trong khung nhìn của mình.
    • Các hệ gợi ý – Tương tự như trên, tôi cũng không bận tâm quá nhiều đến thứ tự ưu tiên của các kết quả gần nhất khi mà tôi chỉ cần khoảng 8 đến 10 kết quả tương tự hiển thị trong khung nhìn của mình.

    Các kỹ thuật ANN tăng tốc độ tìm kiếm bằng cách tiền xử lý dữ liệu thành một chỉ mục hiệu quả và thường được xử lý qua các giai đoạn sau:

    • Vector Transformation – được áp dụng trên các véc-tơ trước khi chúng được lập chỉ mục, ví dụ như giảm chiều dữ liệu.
    • Vector Encoding – được áp dụng trên các véc-tơ để xây dựng chỉ mục thực sự cho tìm kiếm; một số kỹ thuật dựa trên cấu trúc dữ liệu được áp dụng như: Cây, LSH, và lượng tử hóa – một kỹ thuật để mã hóa véc-tơ thành dạng nén, nhỏ gọn hơn nhiều.
    • Thành phần loại bỏ tìm kiếm toàn bộ – được áp dụng trên các véc-tơ để tránh tìm kiếm toàn bộ như k-NN diễn ra, sử dụng các kỹ thuật như: Các tệp đảo ngược, các đồ thị hàng xóm lân cận,…

    Vì sự hữu ích cũng như ứng dụng thực tiễn mà ANN mang lại, nên hiện nay đã có một số thuật toán ANN được triển khai nguồn mở và được sử dụng phổ biến, như: Annoy của Spotify [1], ScaNN của Google [2], Faiss của Facebook [3], và Hnsw [4].

    Tuy nhiên, các kỹ thuật ANN cũng tồn tại nhược điểm, một trong số đó là tài nguyên điện toán, cụ thể là RAM, các kỹ thuật này phải tải toàn bộ các véc-tơ vào RAM để có thể truy xuất các hàng xóm gần nhất.

    Tài liệu tham khảo

    [1] ANNOY library, https://github.com/spotify/annoy

    [2] ScaNN library, https://github.com/google-research/google-research/tree/master/scann

    [3] Faiss library, https://github.com/facebookresearch/faiss

    [4] Hnsw library, https://github.com/nmslib/hnswlib

    Author

    Ha Huu Linh

  • Một số nguyên lý của Elasticsearch

    Một số nguyên lý của Elasticsearch

    Elasticsearch là một công cụ phân tích và tìm kiếm phân tán theo thời gian thực. Nó cho phép khám phá dữ liệu với tốc độ và quy mô chưa từng có trước đây. Nó được sử dụng trong tìm kiếm toàn văn bản (full-text search), tìm kiếm có cấu trúc, phân tích và kết hợp cả ba. Một số hệ thống nổi tiếng sử dụng Elasticsearch có thể kể đến như: GitHub, StackOverflow, Wikipedia,… Elasticsearch còn là một công cụ tìm kiếm nguồn mở, được xây dựng dựa trên Apache Lucene – một thư viện tìm kiếm toàn văn bản.

    Elasticsearch lưu trữ các documents theo mô hình phân tán, các documents là các đối tượng lưu trữ dữ liệu dưới dạng khóa-giá trị (key-value), có thể được chuyển đổi từ định dạng JSON, và thực tế là Elasticsearch nhận các JSON documents để làm đầu vào cho xử lý hoặc để trả về các kết quả cho máy khách. Elasticsearch không chỉ lưu trữ các documents, nó còn lập chỉ mục (indexing) chúng để làm cho chúng có thể tìm kiếm được.

    Elasticsearch cung cấp khả năng mở rộng và tính khả dụng cao nhờ bản chất phân tán, nhờ che giấu toàn bộ việc quản lý hạ tầng giúp cho ứng dụng có thể dễ dàng làm việc với Elasticsearch mà không cần phải có một hiểu biết sâu sắc về vận hành hạ tầng cũng như tổ chức dữ liệu trong Elasticsearch. Tuy nhiên, để có vể vận hành Elasticsearch hiệu quả, việc hiểu cách Elasticsearch tổ chức dữ liệu là cần thiết. Về mặt vật lý, Elasticsearch tổ chức dữ liệu theo 3 cấp độ, với các khái niệm: cluster, node, và shard.

    Một node là một instance đang chạy của Elasticsearch; trong khi đó một cluster bao gồm 1 hoặc nhiều nodes phối hợp để chia sẻ dữ liệu và workload. Khi một node được thêm vào hoặc loại bỏ khỏi cluster thì cluster sẽ tự tổ chức lại để có thể điều phối dữ liệu đều giữa các nodes. Để đạt được điều đó thì một node trong cluster sẽ được chọn làm master node – đóng vai trò như người quản lý, nó chịu trách nhiệm cho việc quản lý các thay đổi trong cluster như thêm hoặc xóa một node khỏi cluster. Master node không tham gia vào các thay đổi hoặc tìm kiếm ở cấp độ document trừ phi cluster chỉ có 1 node duy nhất, việc này giúp cho nó không trở thành bottleneck của hệ thống khi lưu lượng truy cập tăng lên. Trong Elasticsearch thì bất kỳ node nào cũng có thể trở thành master node. Trong khi nói về tổ chức vật lý bên trong Elasticsearch thì không thể không nói về các shards; trong Elasticsearch thì shard là đơn vị vật lý cấp thấp được sử dụng để tổ chức dữ liệu – nơi mà các documents trong một index sẽ được phân bổ trong một vài shards.

    Hình trên minh họa mối quan hệ giữa index và các shards trong Elasticsearch. Về bản chất, index chỉ là một không gian tên logic chứa các documents, các nhà phát triển phần mềm ứng dụng sẽ làm việc với index thay vì trực tiếp với các shards; trong khi đó ở phía dưới, Elasticsearch sẽ tổ chức các documents trong cùng một index vào các shards.

    Như vậy, khi làm việc với Elasticsearch, chúng ta quan tâm chính là về index. Thuật ngữ index có thể được hiểu theo nhiều nghĩa khác nhau, phụ thuộc vào ngữ cảnh: (i) index là một danh từ chỉ nơi lưu trữ các documents, nó giống như một cơ sở dữ liệu trong các cơ sở dữ liệu truyền thống; (ii index cũng có thể được hiểu là một động từ chỉ việc lưu trữ 1 document vào một index (danh từ), thường được gọi là quá trình indexing (lập chỉ mục); và (iii) inverted index chỉ một cấu trúc dữ liệu mà Elasticsearch và Lucene sử dụng để hỗ trợ truy xuất dữ liệu toàn văn bản nhanh chóng.

    Ngày nay, Elasticsearch đã được sử dụng trong rất nhiều ứng dụng nói chung và thương mại điện tử nói riêng bởi nó được thiết kế để hỗ trợ mạnh mẽ trong tìm kiếm và phân tích với dữ liệu. Elasticsearch có thể được triển khai trên các máy chủ tại chỗ (on-premise) nhưng phổ biến hơn hết là trên môi trường đám mây. Vào năm 2015, Amazon Web Services đã triển khai Elasticsearch trên đám mây AWS để cho phép các nhà phát triển phần mềm chạy và vận hành các cụm máy chủ Elasticsearch trên môi trường đám mây. Việc triển khai Elasticsearch trên môi trường đám mây AWS là cần thiết bởi nó cho phép các ứng dụng trên hệ sinh thái AWS có thể dễ dàng giao tiếp, và nhà phát triển phần mềm dễ dàng giám sát hiệu năng hệ thống nhờ sự hỗ trợ gián tiếp từ AWS Cloudwatch và nhiều dịch vụ hạ tầng khác.

    Đến năm 2021, Amazon Web Services khởi chạy dự án nguồn mở OpenSearch, như một bản sao chép từ phiên bản 7.10.2 của Elasticsearch. Và OpenSearch được phát triển, bảo trì và quản lý bởi Amazon Web Services. Dịch vụ hạ tầng từ đó cũng được đổi tên thành AWS OpenSearch.

    Author: Ha Huu Linh

  • Á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.

  • Thiết kế hệ thống

    Thiết kế hệ thống

    Một số suy nghĩ cá nhân về thiết kế hệ thống. Bài viết này chỉ muốn tạo ra một cái nhìn High-level về thiết kế hệ thống, để người đọc có thể cảm nhận được rằng: À, thực ra các hệ thống lớn cũng… chỉ có thế thôi, ez game.

    Đầu tiên, mọi hệ thống phức tạp đều được build từ những hệ thống nhỏ hơn, còn được gọi là các hệ thống con (sub-systems). Như vậy để thiết kế được hệ thống phức tạp thì cần hiểu những hệ thống con. Để hiểu được những hệ thống con thì không cách nào khác ngoài việc tự build những hệ thống con.

    Làm cách nào để ghép các hệ thống con lại với nhau thành một hệ thống phức tạp hơn !? Để làm được điều đó, trước hết cần hiểu các giao thức mạng (Protocols), sau đó là các mô hình truyền thông điệp/giao tiếp giữa các máy chủ ứng dụng.

    Có một số mô hình giao tiếp phổ biến, hầu hết chúng ta đều tiếp cận với mô hình Synchronous trước, hay còn gọi là mô hình Request-Response. Phần lớn sinh viên đại học dừng ở mức này, nhưng như thế là đủ, bởi sinh viên cần quan tâm hơn đến các kiến thức nền tảng.

    Tuy nhiên, các hệ thống hiện nay quá phức tạp, phần lớn workloads là Asynchronous thay vì Synchronous. Chính vì vậy, việc học các mô hình giao tiếp Asynchronous là cần thiết, một số mô hình phổ biến như: Push model, Polling model,… Messgages Broker và Event-bus là hai khái niệm phổ biến trong các hệ thống xử lý Asynchronous workloads. Chúng về bản chất chỉ là 1 layer trung gian giúp de-coupling các hệ thống con với nhau.

    Sau khi nắm được sơ sơ về các building blocks rồi thì chúng ta có xu hướng thiết kế theo “khuôn mẫu” –> Architecture Patterns ra đời. Việc học Architecture Patterns có 2 ý nghĩa: (i) thứ nhất giúp chúng ta build các hệ thống phức tạp nhanh hơn bởi những mẫu sẵn có, và (ii) giúp chúng ta hiểu nhanh hơn về cách giao tiếp giữa các hệ thống con với nhau, bởi con người có xu hướng thích làm theo mẫu (có thằng khác chứng minh rồi)

    Tóm lại, nếu nắm được các building blocks kể trên thì kể ra trên đời này không có hệ thống phức tạp nào là không làm được :)))

    p/s: Bốc phét vậy thôi chứ mình vẫn đang tiếp cận ở High-level, cần nghiên cứu nhiều hơn về networking protocols ? Viết bài là một cách học của mình :))) 

  • Á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é.