Skip to content

13.1.1 Định nghĩa các thực thể miền cho R2DBC

Để tìm hiểu Spring Data R2DBC, chúng ta sẽ tái tạo lại lớp lưu trữ dữ liệu (persistence layer) của ứng dụng Taco Cloud, chỉ tập trung vào các thành phần cần thiết để lưu trữ dữ liệu taco và đơn hàng. Điều này bao gồm tạo các thực thể miền cho TacoOrder, Taco, và Ingredient, cùng với các repository tương ứng cho từng thực thể.

Lớp thực thể miền đầu tiên chúng ta sẽ tạo là lớp Ingredient. Nó sẽ giống như đoạn mã tiếp theo.

Liệt kê 13.1 Lớp thực thể Ingredient dùng cho lưu trữ R2DBC

java
package tacos;

import org.springframework.data.annotation.Id;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@Data
@NoArgsConstructor
@RequiredArgsConstructor
@EqualsAndHashCode(exclude = "id")
public class Ingredient {

  @Id
  private Long id;

  private @NonNull String slug;

  private @NonNull String name;
  private @NonNull Type type;

  public static enum Type {
    WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
  }

}

Như bạn có thể thấy, lớp này không khác nhiều so với các phiên bản trước của lớp Ingredient mà chúng ta đã tạo trước đó. Lưu ý hai điểm đáng chú ý sau:

  • Spring Data R2DBC yêu cầu các thuộc tính phải có setter methods, vì vậy thay vì định nghĩa hầu hết thuộc tính là final, chúng phải là non-final. Nhưng để Lombok có thể tạo constructor với các đối số cần thiết, chúng ta chú thích hầu hết các thuộc tính bằng @NonNull. Điều này sẽ khiến Lombok và chú thích @RequiredArgsConstructor bao gồm các thuộc tính đó trong constructor.
  • Khi lưu một đối tượng thông qua repository của Spring Data R2DBC, nếu thuộc tính ID của đối tượng không phải là null, nó sẽ được xử lý như một bản cập nhật. Trường hợp của Ingredient, thuộc tính id trước đó có kiểu String và được chỉ định khi khởi tạo. Nhưng nếu làm như vậy với Spring Data R2DBC sẽ dẫn đến lỗi. Do đó, ở đây chúng ta chuyển ID dạng chuỗi đó sang thuộc tính mới tên là slug, đóng vai trò như một ID giả cho Ingredient, và sử dụng thuộc tính ID kiểu Long được sinh bởi cơ sở dữ liệu.

Bảng cơ sở dữ liệu tương ứng được định nghĩa trong schema.sql như sau:

sql
create table Ingredient (
  id identity,
  slug varchar(4) not null,
  name varchar(25) not null,
  type varchar(10) not null
);

Lớp thực thể Taco cũng tương đối giống với phiên bản trong Spring Data JDBC, như thể hiện ở đoạn mã tiếp theo.

Liệt kê 13.2 Lớp thực thể Taco dùng cho lưu trữ R2DBC

java
package tacos;

import java.util.HashSet;
import java.util.Set;
import org.springframework.data.annotation.Id;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@Data
@NoArgsConstructor
@RequiredArgsConstructor
public class Taco {

  @Id
  private Long id;

  private @NonNull String name;

  private Set<Long> ingredientIds = new HashSet<>();

  public void addIngredient(Ingredient ingredient) {
    ingredientIds.add(ingredient.getId());
  }

}

Tương tự như lớp Ingredient, chúng ta phải cho phép các trường có setter methods, do đó sử dụng @NonNull thay vì final.

Nhưng điều đặc biệt ở đây là thay vì có một danh sách các đối tượng Ingredient, Taco lại có một Set<Long> chứa ID của các đối tượng Ingredient là thành phần của taco đó. Set được chọn thay vì List để đảm bảo tính duy nhất. Nhưng tại sao chúng ta lại phải dùng Set<Long> thay vì Set<Ingredient> cho danh sách nguyên liệu?

Không giống như các dự án Spring Data khác, Spring Data R2DBC hiện tại không hỗ trợ các mối quan hệ trực tiếp giữa các thực thể (ít nhất là ở thời điểm hiện tại). Là một dự án còn tương đối mới, Spring Data R2DBC vẫn đang xử lý các thách thức liên quan đến việc quản lý mối quan hệ một cách không đồng bộ. Điều này có thể thay đổi ở các phiên bản tương lai của Spring Data R2DBC.

Cho đến lúc đó, chúng ta không thể để Taco tham chiếu đến một danh sách các Ingredient và mong rằng việc lưu trữ sẽ tự động hoạt động. Thay vào đó, chúng ta có một số lựa chọn sau khi xử lý mối quan hệ:

  • Định nghĩa các thực thể với tham chiếu tới ID của các đối tượng liên quan. Trong trường hợp này, cột tương ứng trong bảng cơ sở dữ liệu phải được định nghĩa với kiểu mảng nếu có thể. H2 và PostgreSQL là hai cơ sở dữ liệu hỗ trợ cột dạng mảng, nhưng nhiều hệ khác thì không. Ngoài ra, ngay cả khi cơ sở dữ liệu hỗ trợ cột mảng, có thể sẽ không thể định nghĩa các phần tử là khóa ngoại đến bảng được tham chiếu, khiến không thể đảm bảo tính toàn vẹn tham chiếu.
  • Định nghĩa các thực thể và bảng tương ứng để khớp hoàn hảo với nhau. Đối với các tập hợp, điều này có nghĩa là đối tượng được tham chiếu sẽ có một cột ánh xạ ngược lại đến bảng gốc. Ví dụ, bảng của các đối tượng Taco sẽ cần có một cột chỉ về TacoOrderTaco đó là một phần của nó.
  • Tuần tự hóa (serialize) các thực thể được tham chiếu sang JSON và lưu chuỗi JSON đó vào một cột VARCHAR lớn. Cách này hoạt động tốt nếu không cần truy vấn đến các đối tượng được tham chiếu. Tuy nhiên, cách này có giới hạn về kích thước đối tượng JSON do giới hạn độ dài cột VARCHAR. Hơn nữa, sẽ không có cách nào tận dụng schema cơ sở dữ liệu để đảm bảo tính toàn vẹn tham chiếu, vì các đối tượng được lưu dưới dạng chuỗi đơn giản (có thể chứa bất cứ thứ gì).

Dù không có lựa chọn nào là lý tưởng, sau khi cân nhắc, chúng ta sẽ chọn phương án đầu tiên cho đối tượng Taco. Lớp Taco có một Set<Long> chứa ID của các Ingredient. Điều này có nghĩa là bảng tương ứng phải có một cột dạng mảng để lưu các ID đó. Với cơ sở dữ liệu H2, bảng Taco được định nghĩa như sau:

sql
create table Taco (
  id identity,
  name varchar(50) not null,
  ingredient_ids array
);

Kiểu mảng được dùng trong cột ingredient_ids là đặc thù cho H2. Với PostgreSQL, cột đó có thể được định nghĩa là integer[]. Hãy tham khảo tài liệu của cơ sở dữ liệu bạn chọn để biết chi tiết cách định nghĩa cột dạng mảng. Lưu ý rằng không phải tất cả hệ cơ sở dữ liệu đều hỗ trợ cột mảng, vì vậy bạn có thể cần chọn một trong các cách khác để mô hình hóa quan hệ.

Cuối cùng, lớp TacoOrder, như thể hiện trong đoạn mã tiếp theo, được định nghĩa sử dụng nhiều kỹ thuật mà chúng ta đã áp dụng khi định nghĩa các thực thể miền để lưu trữ với Spring Data R2DBC.

Liệt kê 13.3 Lớp thực thể TacoOrder dùng cho lưu trữ R2DBC

java
package tacos;

import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.data.annotation.Id;
import lombok.Data;

@Data
public class TacoOrder {

  @Id
  private Long id;

  private String deliveryName;
  private String deliveryStreet;
  private String deliveryCity;
  private String deliveryState;
  private String deliveryZip;
  private String ccNumber;
  private String ccExpiration;
  private String ccCVV;

  private Set<Long> tacoIds = new LinkedHashSet<>();

  private List<Taco> tacos = new ArrayList<>();
  public void addTaco(Taco taco) {
    this.tacos.add(taco);
  }

}

Như bạn thấy, ngoài việc có nhiều thuộc tính hơn, lớp TacoOrder tuân theo cùng một mẫu như lớp Taco. Nó tham chiếu các đối tượng Taco con thông qua Set<Long>. Tuy nhiên, một chút nữa, chúng ta sẽ thấy cách để đưa các đối tượng Taco đầy đủ vào TacoOrder, mặc dù Spring Data R2DBC không hỗ trợ trực tiếp các mối quan hệ như vậy.

Schema cơ sở dữ liệu cho bảng Taco_Order như sau:

sql
create table Taco_Order (
  id identity,
  delivery_name varchar(50) not null,
  delivery_street varchar(50) not null,
  delivery_city varchar(50) not null,
  delivery_state varchar(2) not null,
  delivery_zip varchar(10) not null,
  cc_number varchar(16) not null,
  cc_expiration varchar(5) not null,
  cc_cvv varchar(3) not null,
  taco_ids array
);

Tương tự như bảng Taco, bảng TacoOrder tham chiếu đến các Taco con thông qua một cột taco_ids được định nghĩa là cột dạng mảng. Một lần nữa, schema này dành cho cơ sở dữ liệu H2; hãy tham khảo tài liệu của hệ quản trị cơ sở dữ liệu bạn dùng để biết chi tiết hỗ trợ và cách tạo cột mảng.

Thông thường, một ứng dụng sản xuất đã có schema cơ sở dữ liệu được định nghĩa bằng các cách khác, và các script như vậy chỉ thích hợp cho mục đích kiểm thử. Vì vậy, bean này chỉ được định nghĩa trong một cấu hình được load khi chạy kiểm thử tự động và không có mặt trong ngữ cảnh ứng dụng khi chạy thực tế. Chúng ta sẽ xem ví dụ về bài kiểm thử R2DBC repository sau khi đã định nghĩa các service đó.

Ngoài ra, hãy chú ý rằng bean này chỉ sử dụng file schema.sql ở thư mục gốc của classpath (dưới src/main/resources trong dự án). Nếu bạn muốn các script SQL khác được bao gồm trong quá trình khởi tạo cơ sở dữ liệu, hãy thêm nhiều ResourceDatabasePopulator khác vào lời gọi populator.addPopulators().

Giờ khi đã định nghĩa các thực thể và schema cơ sở dữ liệu tương ứng, hãy tạo các repository mà qua đó chúng ta sẽ lưu và truy xuất dữ liệu taco.

Released under the MIT License.