Giản đồ, truy vấn và dữ liệu đột biến của Data Connect

Firebase Data Connect cho phép bạn tạo trình kết nối cho các phiên bản PostgreSQL được quản lý bằng Google Cloud SQL. Các trình kết nối này là sự kết hợp của một giản đồ, truy vấn và đột biến để sử dụng dữ liệu của bạn.

Hướng dẫn bắt đầu đã giới thiệu giản đồ ứng dụng đánh giá phim cho PostgreSQL và hướng dẫn này sẽ xem xét kỹ hơn về cách thiết kế giản đồ Data Connect cho PostgreSQL.

Hướng dẫn này sẽ ghép nối các truy vấn và đột biến Data Connect với các ví dụ về giản đồ. Tại sao chúng ta lại thảo luận về truy vấn (và sự thay đổi) trong hướng dẫn về schema Data Connect? Giống như các nền tảng dựa trên GraphQL khác, Firebase Data Connect là một nền tảng phát triển ưu tiên truy vấn. Vì vậy, với tư cách là nhà phát triển, trong quá trình lập mô hình dữ liệu, bạn sẽ phải suy nghĩ về dữ liệu mà khách hàng của mình cần. Điều này sẽ ảnh hưởng rất lớn đến giản đồ dữ liệu mà bạn phát triển cho dự án của mình.

Hướng dẫn này bắt đầu bằng một giản đồ mới cho bài đánh giá phim, sau đó trình bày về truy vấnsự thay đổi bắt nguồn từ giản đồ đó, cuối cùng cung cấp danh sách SQL tương đương với giản đồ Data Connect cốt lõi.

Sơ đồ cho ứng dụng đánh giá phim

Hãy tưởng tượng bạn muốn xây dựng một dịch vụ cho phép người dùng gửi và xem bài đánh giá phim.

Bạn cần có một giản đồ ban đầu cho ứng dụng như vậy. Bạn sẽ mở rộng giản đồ này sau đó để tạo các truy vấn quan hệ phức tạp.

Bảng phim

Giản đồ cho Phim chứa các lệnh chính như:

  • @table(name)@col(name) để tuỳ chỉnh tên bảng và cột SQL. Data Connect sẽ tạo tên snake_case nếu bạn không chỉ định.
  • @col(dataType) để tuỳ chỉnh các loại cột SQL.
  • @default để định cấu hình giá trị mặc định của cột SQL trong quá trình chèn.

Để biết thêm thông tin chi tiết, hãy xem tài liệu tham khảo về @table, @col, @default.

# Movies
type Movie @table(name: "movie", key: "id") {
  id: UUID! @col(name: "movie_id") @default(expr: "uuidV4()")
  title: String!
  releaseYear: Int
  genre: String @col(dataType: "varchar(20)")
  rating: Int
  description: String
}

Giá trị vô hướng chính và giá trị máy chủ

Trước khi tìm hiểu thêm về ứng dụng đánh giá phim, hãy giới thiệu Data Connect các đại lượng vô hướng chínhgiá trị máy chủ.

Đại lượng vô hướng chính là giá trị nhận dạng đối tượng ngắn gọn mà Data Connect tự động ghép nối từ các trường chính trong giản đồ. Các đại lượng vô hướng chính là về hiệu quả, cho phép bạn tìm thấy trong một thông tin lệnh gọi duy nhất về danh tính và cấu trúc của dữ liệu. Các khoá này đặc biệt hữu ích khi bạn muốn thực hiện các thao tác tuần tự trên các bản ghi mới và cần một giá trị nhận dạng duy nhất để truyền đến các thao tác sắp tới, cũng như khi bạn muốn truy cập vào các khoá quan hệ để thực hiện các thao tác phức tạp hơn.

Khi sử dụng giá trị máy chủ, bạn có thể cho phép máy chủ tự động điền các trường trong bảng bằng cách sử dụng các giá trị đã lưu trữ hoặc dễ dàng tính toán theo các biểu thức CEL cụ thể phía máy chủ trong đối số expr. Ví dụ: bạn có thể xác định một trường có dấu thời gian được áp dụng khi truy cập vào trường đó bằng thời gian được lưu trữ trong yêu cầu thao tác, updatedAt: Timestamp! @default(expr: "request.time").

Bảng siêu dữ liệu của phim

Bây giờ, hãy theo dõi các đạo diễn phim, cũng như thiết lập mối quan hệ một với một với Movie.

Thêm trường tham chiếu để xác định mối quan hệ.

Bạn có thể sử dụng lệnh @ref để tuỳ chỉnh quy tắc ràng buộc khoá ngoại.

  • @ref(fields) để chỉ định các trường khoá ngoại.
  • @ref(references) để chỉ định các trường được tham chiếu trong bảng đích. Tham chiếu này mặc định là khoá chính, nhưng các trường có @unique cũng được hỗ trợ.

Để biết thêm thông tin chi tiết, hãy xem tài liệu tham khảo về @ref.

# Movie Metadata
# Movie - MovieMetadata is a one-to-one relationship
type MovieMetadata @table {
  # @unique ensures that each Movie only has one MovieMetadata.
  movie: Movie! @unique
  # Since it references to another table type, it adds a foreign key constraint.
  #  movie: Movie! @unique @ref(fields: "movieId", references: "id")
  #  movieId: UUID! <- implicitly added foreign key field
  director: String
}

Actor và MovieActor

Tiếp theo, bạn muốn các diễn viên đóng vai chính trong phim của mình. Vì bạn có mối quan hệ nhiều với nhiều giữa phim và diễn viên, hãy tạo một bảng nối.

# Actors
# Suppose an actor can participate in multiple movies and movies can have multiple actors
# Movie - Actors (or vice versa) is a many to many relationship
type Actor @table {
  id: UUID! @default(expr: "uuidV4()")
  name: String! @col(dataType: "varchar(30)")
}
# Join table for many-to-many relationship for movies and actors
# The 'key' param signifies the primary keys of this table
# In this case, the keys are [movieId, actorId], the foreign key fields of the reference fields [movie, actor]
type MovieActor @table(key: ["movie", "actor"]) {
  movie: Movie!
  # movieId: UUID! <- implicitly added foreign key field
  actor: Actor!
  # actorId: UUID! <- implicitly added foreign key field
  role: String! # "main" or "supporting"
  # optional other fields
}

Người dùng

Cuối cùng, người dùng ứng dụng của bạn.

# Users
# Suppose a user can leave reviews for movies
type User @table {
  id: String! @default(expr: "auth.uid")
  username: String! @col(dataType: "varchar(50)")
}

Loại dữ liệu được hỗ trợ

Data Connect hỗ trợ các loại dữ liệu vô hướng sau đây, với các giá trị gán cho các loại PostgreSQL bằng @col(dataType:).

Loại Data Connect Loại tích hợp sẵn GraphQL hoặc loại tuỳ chỉnh
Data Connect
Loại PostgreSQL mặc định Các loại PostgreSQL được hỗ trợ
(bí danh trong ngoặc đơn)
Chuỗi GraphQL văn bản text
bit(n), varbit(n)
char(n), varchar(n)
Int GraphQL int Int2 (smallint, smallserial),
int4 (integer, int, serial)
Nổi GraphQL float8 float4 (thực)
float8 (độ chính xác kép)
số (thập phân)
Boolean GraphQL boolean boolean
mã nhận dạng duy nhất (UUID) Tuỳ chỉnh uuid uuid
Int64 Tuỳ chỉnh bigint int8 (bigint, bigserial)
số (thập phân)
Ngày Tuỳ chỉnh ngày ngày
Dấu thời gian Tuỳ chỉnh timestamptz

timestamptz

Lưu ý: Thông tin về múi giờ tại địa phương không được lưu trữ.
PostgreSQL chuyển đổi và lưu trữ các dấu thời gian đó dưới dạng UTC.

Vectơ Tuỳ chỉnh vectơ

vectơ

Xem bài viết Tìm kiếm độ tương đồng vectơ bằng Vertex AI.

  • GraphQL List liên kết đến một mảng một chiều.
    • Ví dụ: [Int] liên kết đến int5[], [Any] liên kết đến jsonb[].
    • Data Connect không hỗ trợ mảng lồng nhau.

Sử dụng các trường đã tạo để tạo truy vấn và đột biến

Các truy vấn và đột biến Data Connect sẽ mở rộng một tập hợp các trường Data Connect được tạo tự động dựa trên các loại và mối quan hệ loại trong giản đồ của bạn. Các trường này được tạo bằng công cụ cục bộ mỗi khi bạn chỉnh sửa giản đồ.

  • Như bạn đã thấy trong hướng dẫn Bắt đầu, bảng điều khiển Firebase và công cụ phát triển cục bộ của chúng tôi sử dụng các trường được tạo tự động này để cung cấp cho bạn các truy vấn và đột biến quản trị đặc biệt mà bạn có thể sử dụng để tạo dữ liệu và xác minh nội dung của bảng.

  • Trong quá trình phát triển, bạn sẽ triển khai truy vấn có thể triển khaicác đột biến có thể triển khai được đóng gói trong trình kết nối, dựa trên các trường được tạo tự động này.

Tên trường được tạo tự động

Data Connect suy luận tên phù hợp cho các trường được tạo tự động dựa trên nội dung khai báo loại giản đồ. Ví dụ: khi làm việc với nguồn PostgreSQL, nếu bạn xác định một bảng có tên là Movie, máy chủ sẽ tạo:

  • Các trường để đọc dữ liệu trong trường hợp sử dụng bảng đơn có tên thân thiện movie (số ít, để truy xuất kết quả riêng lẻ truyền các đối số như eq) và movies (số nhiều, để truy xuất danh sách kết quả truyền các đối số như gt và các thao tác như orderby). Data Connect cũng tạo các trường cho các thao tác đa bảng, quy chiếu có tên rõ ràng như actors_on_movies hoặc actors_via_actormovie.
  • Các trường để ghi dữ liệu có tên quen thuộc như movie_insert, movie_upsert...

Ngôn ngữ định nghĩa giản đồ cũng cho phép bạn kiểm soát rõ ràng cách tạo tên cho các trường bằng cách sử dụng đối số lệnh singularplural.

Chỉ thị cho truy vấn và đột biến

Ngoài các lệnh mà bạn sử dụng để xác định loại và bảng, Data Connect còn cung cấp các lệnh @auth, @check, @redact@transaction để tăng cường hành vi của truy vấn và đột biến.

Chỉ thị Áp dụng cho Mô tả
@auth Truy vấn và đột biến Xác định chính sách uỷ quyền cho một truy vấn hoặc đột biến. Xem hướng dẫn uỷ quyền và chứng thực.
@check Các trường query trong các thao tác nhiều bước Xác minh rằng các trường được chỉ định có trong kết quả truy vấn. Biểu thức Ngôn ngữ diễn đạt thông thường (CEL) được dùng để kiểm thử giá trị trường. Xem phần Thao tác nhiều bước.
@redact Cụm từ tìm kiếm Xoá một phần nội dung phản hồi của ứng dụng khách. Xem phần Thao tác nhiều bước.
@transaction Đột biến Thực thi để một đột biến luôn chạy trong một giao dịch cơ sở dữ liệu. Xem phần Thao tác nhiều bước.

Truy vấn cho cơ sở dữ liệu bài đánh giá phim

Bạn xác định truy vấn Data Connect bằng cách khai báo loại thao tác truy vấn, tên thao tác, không có hoặc có đối số thao tác và không có hoặc có lệnh với đối số.

Trong phần hướng dẫn nhanh, truy vấn listEmails mẫu không có tham số nào. Tất nhiên, trong nhiều trường hợp, dữ liệu được truyền đến các trường truy vấn sẽ mang tính động. Bạn có thể sử dụng cú pháp $variableName để xử lý các biến dưới dạng một trong các thành phần của định nghĩa truy vấn.

Vì vậy, truy vấn sau đây có:

  • Định nghĩa kiểu query
  • Tên toán tử (truy vấn) ListMoviesByGenre
  • Một đối số thao tác $genre biến
  • Một lệnh duy nhất, @auth.
query ListMoviesByGenre($genre: String!) @auth(level: USER)

Mỗi đối số truy vấn đều yêu cầu một nội dung khai báo kiểu, một nội dung tích hợp sẵn như String hoặc một kiểu tuỳ chỉnh, do giản đồ xác định như Movie.

Hãy xem chữ ký của các truy vấn ngày càng phức tạp. Cuối cùng, bạn sẽ được giới thiệu về các biểu thức mối quan hệ mạnh mẽ và ngắn gọn mà bạn có thể sử dụng để tạo các truy vấn có thể triển khai.

Số thực chính trong truy vấn

Nhưng trước tiên, hãy lưu ý về đại lượng vô hướng chính.

Data Connect xác định một loại đặc biệt cho các đại lượng vô hướng chính, được xác định bằng _Key. Ví dụ: loại của một đại lượng vô hướng khoá cho bảng MovieMovie_Key.

Bạn truy xuất các đại lượng vô hướng chính dưới dạng phản hồi do hầu hết các trường đọc được tạo tự động trả về, hoặc tất nhiên là từ các truy vấn mà bạn đã truy xuất tất cả các trường cần thiết để tạo khoá vô hướng.

Các truy vấn tự động số ít, chẳng hạn như movie trong ví dụ đang chạy, hỗ trợ một đối số khoá chấp nhận một đại lượng vô hướng khoá.

Bạn có thể truyền một đại lượng vô hướng khoá dưới dạng giá trị cố định. Tuy nhiên, bạn có thể xác định các biến để truyền các đại lượng vô hướng khoá làm dữ liệu đầu vào.

query GetMovie($myKey: Movie_Key!) {
  movie(key: $myKey) { title }
}

Bạn có thể cung cấp các thông tin này trong yêu cầu JSON như sau (hoặc các định dạng chuyển đổi tuần tự khác):

{
  # 
  "variables": {
    "myKey": {"foo": "some-string-value", "bar": 42}
  }
}

Nhờ khả năng phân tích cú pháp vô hướng tuỳ chỉnh, bạn cũng có thể tạo Movie_Key bằng cú pháp đối tượng, cú pháp này có thể chứa các biến. Điều này chủ yếu hữu ích khi bạn muốn chia các thành phần riêng lẻ thành nhiều biến vì lý do nào đó.

Bí danh trong truy vấn

Data Connect hỗ trợ việc gán bí danh GraphQL trong các truy vấn. Với bí danh, bạn đổi tên dữ liệu được trả về trong kết quả của truy vấn. Một truy vấn Data Connect có thể áp dụng nhiều bộ lọc hoặc các thao tác truy vấn khác trong một yêu cầu hiệu quả đến máy chủ, đồng thời phát hành một số "truy vấn phụ" một cách hiệu quả. Để tránh xung đột tên trong tập dữ liệu được trả về, bạn sử dụng bí danh để phân biệt các truy vấn phụ.

Dưới đây là một truy vấn trong đó biểu thức sử dụng bí danh mostPopular.

query ReviewTopPopularity($genre: String) {
  mostPopular: review(first: {
    where: {genre: {eq: $genre}},
    orderBy: {popularity: DESC}
  }) {  }
}

Truy vấn đơn giản có bộ lọc

Truy vấn Data Connect liên kết với tất cả bộ lọc SQL và thao tác thứ tự phổ biến.

Toán tử whereorderBy (truy vấn số ít, số nhiều)

Trả về tất cả các hàng đã so khớp trong bảng (và các mối liên kết lồng nhau). Trả về một mảng trống nếu không có bản ghi nào khớp với bộ lọc.

query MovieByTopRating($genre: String) {
  mostPopular: movies(
     where: { genre: { eq: $genre } }, orderBy: { rating: DESC }
  ) {
    # graphql: list the fields from the results to return
    id
    title
    genre
    description
  }
}

query MoviesByReleaseYear($min: Int, $max: Int) {
  movies(where: {releaseYear: {le: $max, ge: $min}}, orderBy: [{releaseYear: ASC}]) {  }
}

Toán tử limitoffset (truy vấn số ít, số nhiều)

Bạn có thể phân trang kết quả. Các đối số này được chấp nhận nhưng không được trả về trong kết quả.

query MoviesTop10 {
  movies(orderBy: [{ rating: DESC }], limit: 10) {
    # graphql: list the fields from the results to return
    title
  }
}

bao gồm các trường mảng

Bạn có thể kiểm tra để đảm bảo rằng một trường mảng có chứa một mục được chỉ định.

# Filter using arrays and embedded fields.
query ListMoviesByTag($tag: String!) {
  movies(where: { tags: { includes: $tag }}) {
    # graphql: list the fields from the results to return
    id
    title
  }
}

Thao tác với chuỗi và biểu thức chính quy

Truy vấn của bạn có thể sử dụng các thao tác so sánh và tìm kiếm chuỗi thông thường, bao gồm cả biểu thức chính quy. Lưu ý để tăng hiệu quả, bạn đang gộp một số thao tác ở đây và phân biệt các thao tác đó bằng bí danh.

query MoviesTitleSearch($prefix: String, $suffix: String, $contained: String, $regex: String) {
  prefixed: movies(where: {title: {startsWith: $prefix}}) {...}
  suffixed: movies(where: {title: {endsWith: $suffix}}) {...}
  contained: movies(where: {title: {contains: $contained}}) {...}
  matchRegex: movies(where: {title: {pattern: {regex: $regex}}}) {...}
}

orand cho bộ lọc tổng hợp

Sử dụng orand cho logic phức tạp hơn.

query ListMoviesByGenreAndGenre($minRating: Int!, $genre: String) {
  movies(
    where: { _or: [{ rating: { ge: $minRating } }, { genre: { eq: $genre } }] }
  ) {
    # graphql: list the fields from the results to return
    title
  }
}

Câu lệnh truy vấn phức tạp

Truy vấn Data Connect có thể truy cập vào dữ liệu dựa trên mối quan hệ giữa các bảng. Bạn có thể sử dụng mối quan hệ đối tượng (một với một) hoặc mảng (một với nhiều) được xác định trong giản đồ để tạo truy vấn lồng nhau, tức là tìm nạp dữ liệu cho một loại cùng với dữ liệu từ một loại lồng nhau hoặc có liên quan.

Các truy vấn như vậy sử dụng cú pháp Data Connect _on__via ma thuật trong các trường đọc được tạo.

Bạn sẽ sửa đổi giản đồ từ phiên bản ban đầu của chúng tôi.

Nhiều với một

Hãy thêm bài đánh giá vào ứng dụng của chúng ta, với bảng Review và các sửa đổi đối với User.

# User table is keyed by Firebase Auth UID.
type User @table {
  # `@default(expr: "auth.uid")` sets it to Firebase Auth UID during insert and upsert.
  id: String! @default(expr: "auth.uid")
  username: String! @col(dataType: "varchar(50)")
  # The `user: User!` field in the Review table generates the following one-to-many query field.
  #  reviews_on_user: [Review!]!
  # The `Review` join table the following many-to-many query field.
  #  movies_via_Review: [Movie!]!
}

# Reviews is a join table tween User and Movie.
# It has a composite primary keys `userUid` and `movieId`.
# A user can leave reviews for many movies. A movie can have reviews from many users.
# User  <-> Review is a one-to-many relationship
# Movie <-> Review is a one-to-many relationship
# Movie <-> User is a many-to-many relationship
type Review @table(name: "Reviews", key: ["movie", "user"]) {
  user: User!
  # The user field adds the following foreign key field. Feel free to uncomment and customize it.
  #  userUid: String!
  movie: Movie!
  # The movie field adds the following foreign key field. Feel free to uncomment and customize it.
  #  movieId: UUID!
  rating: Int
  reviewText: String
  reviewDate: Date! @default(expr: "request.time")
}

Truy vấn nhiều với một

Bây giờ, hãy xem một truy vấn có sử dụng bí danh để minh hoạ cú pháp _via_.

query UserMoviePreferences($username: String!) @auth(level: USER) {
  users(where: { username: { eq: $username } }) {
    likedMovies: movies_via_Review(where: { rating: { ge: 4 } }) {
      title
      genre
    }
    dislikedMovies: movies_via_Review(where: { rating: { le: 2 } }) {
      title
      genre
    }
  }
}

Một với một

Bạn có thể thấy mẫu này. Dưới đây là giản đồ được sửa đổi để minh hoạ.

# Movies
type Movie
  @table(name: "Movies", singular: "movie", plural: "movies", key: ["id"]) {
  id: UUID! @col(name: "movie_id") @default(expr: "uuidV4()")
  title: String!
  releaseYear: Int @col(name: "release_year")
  genre: String
  rating: Int @col(name: "rating")
  description: String @col(name: "description")
  tags: [String] @col(name: "tags")
}
# Movie Metadata
# Movie - MovieMetadata is a one-to-one relationship
type MovieMetadata
  @table(
    name: "MovieMetadata"
  ) {
  # @ref creates a field in the current table (MovieMetadata) that holds the primary key of the referenced type
  # In this case, @ref(fields: "id") is implied
  movie: Movie! @ref
  # movieId: UUID <- this is created by the above @ref
  director: String @col(name: "director")
}


extend type MovieMetadata {
  movieId: UUID! # matches primary key of referenced type
...
}

extend type Movie {
  movieMetadata: MovieMetadata # can only be non-nullable on ref side
  # conflict-free name, always generated
  movieMetadatas_on_movie: MovieMetadata
}

Truy vấn một với một

Bạn có thể truy vấn bằng cú pháp _on_.

# One to one
query GetMovieMetadata($id: UUID!) @auth(level: PUBLIC) {
  movie(id: $id) {
    movieMetadatas_on_movie {
      director
    }
  }
}

Nhiều với nhiều

Phim cần diễn viên và diễn viên cần phim. Các lớp này có mối quan hệ nhiều với nhiều mà bạn có thể lập mô hình bằng bảng nối MovieActors.

# MovieActors Join Table Definition
type MovieActors @table(
  key: ["movie", "actor"] # join key triggers many-to-many generation
) {
  movie: Movie!
  actor: Actor!
}

# generated extensions for the MovieActors join table
extend type MovieActors {
  movieId: UUID!
  actorId: UUID!
}

# Extensions for Actor and Movie to handle many-to-many relationships
extend type Movie {
  movieActors: [MovieActors!]! # standard many-to-one relation to join table
  actors: [Actor!]! # many-to-many via join table

  movieActors_on_actor: [MovieActors!]!
  # since MovieActors joins distinct types, type name alone is sufficiently precise
  actors_via_MovieActors: [Actor!]!
}

extend type Actor {
  movieActors: [MovieActors!]! # standard many-to-one relation to join table
  movies: [Movie!]! # many-to-many via join table

  movieActors_on_movie: [MovieActors!]!
  movies_via_MovieActors: [Movie!]!
}

Truy vấn nhiều với nhiều

Hãy xem một truy vấn có sử dụng bí danh để minh hoạ cú pháp _via_.

query GetMovieCast($movieId: UUID!, $actorId: UUID!) @auth(level: PUBLIC) {
  movie(id: $movieId) {
    mainActors: actors_via_MovieActor(where: { role: { eq: "main" } }) {
      name
    }
    supportingActors: actors_via_MovieActor(
      where: { role: { eq: "supporting" } }
    ) {
      name
    }
  }
  actor(id: $actorId) {
    mainRoles: movies_via_MovieActor(where: { role: { eq: "main" } }) {
      title
    }
    supportingRoles: movies_via_MovieActor(
      where: { role: { eq: "supporting" } }
    ) {
      title
    }
  }
}

Truy vấn tổng hợp

Dữ liệu tổng hợp là gì và tại sao nên sử dụng dữ liệu tổng hợp?

Trường tổng hợp cho phép bạn thực hiện các phép tính trên danh sách kết quả. Với các trường tổng hợp, bạn có thể làm những việc như:

  • Tìm điểm trung bình của một bài đánh giá
  • Tìm tổng chi phí của các mặt hàng trong giỏ hàng
  • Tìm sản phẩm được xếp hạng cao nhất hoặc thấp nhất
  • Đếm số lượng sản phẩm trong cửa hàng

Các phép tổng hợp được thực hiện trên máy chủ, mang lại một số lợi ích so với việc tính toán các phép tổng hợp đó ở phía máy khách:

  • Hiệu suất ứng dụng nhanh hơn (vì bạn tránh được các phép tính phía máy khách)
  • Giảm chi phí truyền dữ liệu ra bên ngoài (vì bạn chỉ gửi kết quả tổng hợp thay vì tất cả dữ liệu đầu vào)
  • Cải thiện khả năng bảo mật (vì bạn có thể cấp cho ứng dụng quyền truy cập vào dữ liệu tổng hợp thay vì toàn bộ tập dữ liệu)

Giản đồ mẫu cho dữ liệu tổng hợp

Trong phần này, chúng ta sẽ chuyển sang giản đồ ví dụ về cửa hàng trực tuyến. Đây là một ví dụ phù hợp để giải thích cách sử dụng dữ liệu tổng hợp:

  type Product @table {
    name: String!
    manufacturer: String!
    quantityInStock: Int!
    price: Float!
    expirationDate: Date
  }

Số liệu tổng hợp đơn giản

_count cho tất cả các trường

Trường tổng hợp đơn giản nhất là _count: trường này trả về số lượng hàng khớp với truy vấn của bạn. Đối với mỗi trường trong loại của bạn, Data Connect sẽ tạo các trường tổng hợp tương ứng tuỳ thuộc vào loại trường.

Truy vấn

query CountProducts {
  products {
    _count
  }
}

Phản hồi
one

Ví dụ: nếu bạn có 5 sản phẩm trong cơ sở dữ liệu, kết quả sẽ là:

{
  "products": [
    {
    "_count": 5
    }
  ]
}

Tất cả các trường đều có trường <field>_count, giúp tính số dòng có giá trị khác rỗng trong trường đó.

Truy vấn

query CountProductsWithExpirationDate {
  products {
    expirationDate_count
  }
}

Phản hồi
field_count

Ví dụ: nếu bạn có 3 sản phẩm có ngày hết hạn, kết quả sẽ là:

{
  "products": [
    {
    "expirationDate_count": 3
    }
  ]
}
_min, _max, _sum và _avg cho các trường số

Các trường số (int, float, int64) cũng có <field>_min, <field>_max, <field>_sum<field>_avg.

Truy vấn

query NumericAggregates {
  products {
  quantityInStock_max
  price_min
  price_avg
  quantityInStock_sum
  }
}

Phản hồi
_min _max _sum _avg

Ví dụ: nếu bạn có các sản phẩm sau:

  • Sản phẩm A: quantityInStock: 10, price: 2.99
  • Sản phẩm B: quantityInStock: 5, price: 5.99
  • Sản phẩm C: quantityInStock: 20, price: 1.99

Kết quả sẽ là:

{
  "products": [
    {
    "quantityInStock_max": 20,
    "price_min": 1.99,
    "price_avg": 3.6566666666666666,
    "quantityInStock_sum": 35
    }
  ]
}
_min và _max cho ngày và dấu thời gian

Các trường ngày và dấu thời gian có <field>_min<field>_max.

Truy vấn

query DateAndTimeAggregates {
  products {
  expirationDate_max
  expirationDate_min
  }
}

Phản hồi
_min _maxdatetime

Ví dụ: nếu bạn có các ngày hết hạn sau:

  • Sản phẩm A: 2024-01-01
  • Sản phẩm B: 2024-03-01
  • Sản phẩm C: 2024-02-01

Kết quả sẽ là:

{
  "products": [
    {
    "expirationDate_max": "2024-03-01",
    "expirationDate_min": "2024-01-01"
    }
  ]
}

Riêng biệt

Đối số distinct cho phép bạn lấy tất cả giá trị duy nhất cho một trường (hoặc tổ hợp các trường). Ví dụ:

Truy vấn

query ListDistinctManufacturers {
  products(distinct: true) {
    manufacturer
  }
}

Phản hồi
distinct

Ví dụ: nếu bạn có các nhà sản xuất sau:

  • Sản phẩm A: manufacturer: "Acme"
  • Sản phẩm B: manufacturer: "Beta"
  • Sản phẩm C: manufacturer: "Acme"

Kết quả sẽ là:

{
  "products": [
    { "manufacturer": "Acme" },
    { "manufacturer": "Beta" }
  ]
}

Bạn cũng có thể sử dụng đối số distinct trên các trường tổng hợp để tổng hợp các giá trị riêng biệt. Ví dụ:

Truy vấn

query CountDistinctManufacturers {
  products {
    manufacturer_count(distinct: true)
  }
}

Phản hồi
distinctonaggregate

Ví dụ: nếu bạn có các nhà sản xuất sau:

  • Sản phẩm A: manufacturer: "Acme"
  • Sản phẩm B: manufacturer: "Beta"
  • Sản phẩm C: manufacturer: "Acme"

Kết quả sẽ là:

{
  "products": [
    {
    "manufacturer_count": 2
    }
  ]
}

Dữ liệu tổng hợp theo nhóm

Bạn thực hiện tổng hợp theo nhóm bằng cách chọn kết hợp các trường tổng hợp và trường không tổng hợp trên một loại. Hàm này nhóm tất cả các hàng trùng khớp có cùng giá trị cho các trường không tổng hợp và tính toán các trường tổng hợp cho nhóm đó. Ví dụ:

Truy vấn

query MostExpensiveProductByManufacturer {
  products {
  manufacturer
  price_max
  }
}

Phản hồi
groupedaggregates

Ví dụ: nếu bạn có các sản phẩm sau:

  • Sản phẩm A: manufacturer: "Acme", price: 2.99
  • Sản phẩm B: manufacturer: "Beta", price: 5.99
  • Sản phẩm C: manufacturer: "Acme", price: 1.99

Kết quả sẽ là:

{
  "products": [
    { "manufacturer": "Acme", "price_max": 2.99 },
    { "manufacturer": "Beta", "price_max": 5.99 }
  ]
}
havingwhere với dữ liệu tổng hợp theo nhóm

Bạn cũng có thể sử dụng đối số havingwhere để chỉ trả về các nhóm đáp ứng tiêu chí đã cung cấp.

  • having cho phép bạn lọc các nhóm theo trường tổng hợp
  • where cho phép bạn lọc các hàng dựa trên các trường không tổng hợp.

Truy vấn

query FilteredMostExpensiveProductByManufacturer {
  products(having: {price_max: {ge: 2.99}}) {
  manufacturer
  price_max
  }
}

Phản hồi
havingwhere

Ví dụ: nếu bạn có các sản phẩm sau:

  • Sản phẩm A: manufacturer: "Acme", price: 2.99
  • Sản phẩm B: manufacturer: "Beta", price: 5.99
  • Sản phẩm C: manufacturer: "Acme", price: 1.99

Kết quả sẽ là:

{
  "products": [
    { "manufacturer": "Acme", "price_max": 2.99 },
    { "manufacturer": "Beta", "price_max": 5.99 }
  ]
}

Tổng hợp trên các bảng

Bạn có thể sử dụng các trường tổng hợp cùng với các trường mối quan hệ một với nhiều được tạo để trả lời các câu hỏi phức tạp về dữ liệu của mình. Dưới đây là một giản đồ đã sửa đổi, với bảng riêng biệt, Manufacturer, chúng ta có thể sử dụng trong các ví dụ:

  type Product @table {
    name: String!
    manufacturer: Manufacturer!
    quantityInStock: Int!
    price: Float!
    expirationDate: Date
  }

  type Manufacturer @table {
    name: String!
    headquartersCountry: String!
  }

Bây giờ, chúng ta có thể sử dụng các trường tổng hợp để thực hiện các thao tác như tìm số lượng sản phẩm mà một nhà sản xuất tạo ra:

Truy vấn

query GetProductCount($id: UUID) {
  manufacturers {
    name
    products_on_manufacturer {
      _count
    }
  }
}

Phản hồi
aggregatesacrosstables

Ví dụ: nếu bạn có các nhà sản xuất sau:

  • Nhà sản xuất A: name: "Acme", products_on_manufacturer: 2
  • Nhà sản xuất B: name: "Beta", products_on_manufacturer: 1

Kết quả sẽ là:

{
  "manufacturers": [
    { "name": "Acme", "products_on_manufacturer": { "_count": 2 } },
    { "name": "Beta", "products_on_manufacturer": { "_count": 1 } }
  ]
}

Các đột biến cho cơ sở dữ liệu bài đánh giá phim

Như đã đề cập, khi bạn xác định một bảng trong giản đồ, Data Connect sẽ tạo các trường _insert, _update cơ bản, v.v. cho mỗi bảng.

type Movie @table { ... }

extend type Mutation {
  # Insert a row into the movie table.
  movie_insert(...): Movie_Key!
  # Upsert a row into movie."
  movie_upsert(...): Movie_Key!
  # Update a row in Movie. Returns null if a row with the specified id/key does not exist
  movie_update(...): Movie_Key
  # Update rows based on a filter in Movie.
  movie_updateMany(...): Int!
  # Delete a single row in Movie. Returns null if a row with the specified id/key does not exist
  movie_delete(...): Movie_Key
  # Delete rows based on a filter in Movie.
  movie_deleteMany(...): Int!
}

Với những kiến thức này, bạn có thể triển khai các trường hợp CRUD cốt lõi ngày càng phức tạp. Hãy nói nhanh năm lần!

Tạo

Hãy tạo các thành phần cơ bản.

# Create a movie based on user input
mutation CreateMovie($title: String!, $releaseYear: Int!, $genre: String!, $rating: Int!) {
  movie_insert(data: {
    title: $title
    releaseYear: $releaseYear
    genre: $genre
    rating: $rating
  })
}

# Create a movie with default values
mutation CreateMovie2 {
  movie_insert(data: {
    title: "Sherlock Holmes"
    releaseYear: 2009
    genre: "Mystery"
    rating: 5
  })
}

Hoặc một thao tác chèn mới.

# Movie upsert using combination of variables and literals
mutation UpsertMovie($title: String!) {
  movie_upsert(data: {
    title: $title
    releaseYear: 2009
    genre: "Mystery"
    rating: 5
    genre: "Mystery/Thriller"
  })
}

Thực hiện cập nhật

Sau đây là thông tin cập nhật. Nhà sản xuất và đạo diễn chắc chắn hy vọng những điểm xếp hạng trung bình đó sẽ tăng lên.

Trường movie_update chứa đối số id dự kiến để xác định một bản ghi và trường data mà bạn có thể sử dụng để đặt giá trị trong bản cập nhật này.

mutation UpdateMovie(
  $id: UUID!,
  $genre: String!,
  $rating: Int!,
  $description: String!
) {
  movie_update(id: $id,
    data: {
      genre: $genre
      rating: $rating
      description: $description
    })
}

Để thực hiện nhiều lần cập nhật, hãy sử dụng trường movie_updateMany.

# Multiple updates (increase all ratings of a genre)
mutation IncreaseRatingForGenre($genre: String!, $rating: Int!) {
  movie_updateMany(
    where: { genre: { eq: $genre } },
    data:
      {
        rating: $rating
      })
}

Sử dụng các thao tác tăng, giảm, nối và thêm vào đầu bằng _update

Mặc dù trong các đột biến _update_updateMany, bạn có thể đặt giá trị một cách rõ ràng trong data:, nhưng thường thì bạn nên áp dụng một toán tử như tăng để cập nhật giá trị.

Để sửa đổi ví dụ cập nhật trước đó, giả sử bạn muốn tăng điểm xếp hạng của một bộ phim cụ thể. Bạn có thể sử dụng cú pháp rating_update với toán tử inc.

mutation UpdateMovie(
  $id: UUID!,
  $ratingIncrement: Int!
) {
  movie_update(id: $id, data: {
    rating_update: {
      inc: $ratingIncrement
    }
  })
}

Data Connect hỗ trợ các toán tử sau đây để cập nhật trường:

  • inc để tăng các loại dữ liệu Int, Int64, Float, DateTimestamp
  • dec để giảm các loại dữ liệu Int, Int64, Float, DateTimestamp

Đối với danh sách, bạn cũng có thể cập nhật bằng các giá trị riêng lẻ hoặc danh sách giá trị bằng cách sử dụng:

  • add để nối(các) mục nếu các mục đó chưa có trong loại danh sách, ngoại trừ danh sách Vectơ
  • remove để xoá tất cả các mục (nếu có) khỏi các loại danh sách, ngoại trừ danh sách Vectơ
  • append để nối(các) mục vào các loại danh sách, ngoại trừ danh sách Vectơ
  • prepend để thêm(các) mục vào loại danh sách, ngoại trừ danh sách Vectơ

Thực hiện xoá

Tất nhiên, bạn có thể xoá dữ liệu phim. Những người bảo tồn phim chắc chắn sẽ muốn duy trì phim vật lý lâu nhất có thể.

# Delete by key
mutation DeleteMovie($id: UUID!) {
  movie_delete(id: $id)
}

Tại đây, bạn có thể sử dụng _deleteMany.

# Multiple deletes
mutation DeleteUnpopularMovies($minRating: Int!) {
  movie_deleteMany(where: { rating: { le: $minRating } })
}

Viết các đột biến trên mối quan hệ

Quan sát cách sử dụng đột biến _upsert ngầm ẩn trên một mối quan hệ.

# Create or update a one to one relation
mutation MovieMetadataUpsert($movieId: UUID!, $director: String!) {
  movieMetadata_upsert(
    data: { movie: { id: $movieId }, director: $director }
  )
}

Cho phép Data Connect cung cấp giá trị bằng cú pháp field_expr

Như đã thảo luận trong phần giá trị vô hướng khoá và giá trị máy chủ, bạn có thể thiết kế giản đồ để máy chủ điền sẵn giá trị cho các trường phổ biến như id và ngày tháng để phản hồi các yêu cầu của ứng dụng.

Ngoài ra, bạn có thể sử dụng dữ liệu, chẳng hạn như mã nhận dạng người dùng, được gửi trong các đối tượng Data Connect request từ ứng dụng khách.

Khi triển khai các đột biến, hãy sử dụng cú pháp field_expr để kích hoạt các bản cập nhật do máy chủ tạo hoặc truy cập dữ liệu từ các yêu cầu. Ví dụ: để truyền uid uỷ quyền được lưu trữ trong một yêu cầu đến một thao tác _upsert, hãy truyền "auth.uid" trong trường userId_expr.

# Add a movie to the user's favorites list
mutation AddFavoritedMovie($movieId: UUID!) @auth(level: USER) {
  favorite_movie_upsert(data: { userId_expr: "auth.uid", movieId: $movieId })
}

# Remove a movie from the user's favorites list
mutation DeleteFavoritedMovie($movieId: UUID!) @auth(level: USER) {
  favorite_movie_delete(key: { userId_expr: "auth.uid", movieId: $movieId })
}

Hoặc trong một ứng dụng danh sách việc cần làm quen thuộc, khi tạo danh sách việc cần làm mới, bạn có thể truyền id_expr để hướng dẫn máy chủ tự động tạo UUID cho danh sách.

mutation CreateTodoListWithFirstItem(
  $listName: String!
) @transaction {
  # Step 1
  todoList_insert(data: {
    id_expr: "uuidV4()", # <-- auto-generated. Or a column-level @default on `type TodoList` will also work
    name: $listName,
  })
}

Để biết thêm thông tin, hãy xem các đại lượng vô hướng _Expr trong tài liệu tham khảo về đại lượng vô hướng.

Thao tác nhiều bước

Có nhiều trường hợp bạn có thể muốn đưa nhiều trường ghi (như chèn) vào một đột biến. Bạn cũng có thể muốn đọc cơ sở dữ liệu trong quá trình thực thi một đột biến để tra cứu và xác minh dữ liệu hiện có trước khi thực hiện, ví dụ: chèn hoặc cập nhật. Các tuỳ chọn này giúp tiết kiệm các thao tác đi và về, từ đó tiết kiệm chi phí.

Data Connect cho phép bạn thực hiện logic nhiều bước trong các đột biến bằng cách hỗ trợ:

  • Nhiều trường ghi

  • Nhiều trường đọc trong các đột biến (sử dụng từ khoá trường query).

  • Lệnh @transaction, cung cấp tính năng hỗ trợ giao dịch quen thuộc từ các cơ sở dữ liệu quan hệ.

  • Lệnh @check cho phép bạn đánh giá nội dung của các lượt đọc bằng biểu thức CEL và dựa trên kết quả của quá trình đánh giá đó:

    • Tiếp tục tạo, cập nhật và xoá do một đột biến xác định
    • Tiếp tục trả về kết quả của trường truy vấn
    • Sử dụng thông báo trả về để thực hiện logic thích hợp trong mã ứng dụng
  • Lệnh @redact cho phép bạn bỏ qua kết quả trường truy vấn khỏi kết quả giao thức truyền dẫn.

  • Liên kết response CEL, lưu trữ kết quả tích luỹ của tất cả các biến thể và truy vấn được thực hiện trong một thao tác phức tạp, nhiều bước. Bạn có thể truy cập vào liên kết response:

    • Trong các lệnh @check, thông qua đối số expr:
    • Với giá trị máy chủ, hãy sử dụng cú pháp field_expr

Chỉ thị @transaction

Tính năng hỗ trợ cho các đột biến nhiều bước bao gồm cả việc xử lý lỗi bằng các giao dịch.

Chỉ thị @transaction thực thi việc một đột biến – với một trường ghi duy nhất (ví dụ: _insert hoặc _update) hoặc với nhiều trường ghi – luôn chạy trong một giao dịch cơ sở dữ liệu.

  • Các đột biến không có @transaction sẽ thực thi từng trường gốc theo trình tự. Thao tác này hiển thị mọi lỗi dưới dạng lỗi một phần trường, nhưng không hiển thị tác động của các lần thực thi tiếp theo.

  • Các đột biến có @transaction được đảm bảo là thành công hoàn toàn hoặc không thành công hoàn toàn. Nếu có trường nào trong giao dịch không thành công, toàn bộ giao dịch sẽ được hoàn tác.

Lệnh @check@redact

Lệnh @check xác minh rằng các trường đã chỉ định có trong kết quả truy vấn. Biểu thức Ngôn ngữ diễn đạt thông thường (CEL) được dùng để kiểm thử giá trị trường. Hành vi mặc định của lệnh này là kiểm tra và từ chối các nút có giá trị là null hoặc [] (danh sách trống).

Chỉ thị @redact sẽ loại bỏ một phần phản hồi từ ứng dụng. Các trường đã bị loại bỏ vẫn được đánh giá để xem có hiệu ứng phụ nào không (bao gồm cả thay đổi về dữ liệu và @check) và kết quả vẫn có sẵn cho các bước sau trong biểu thức CEL.

Sử dụng @check, @check(message:)@redact

Một mục đích sử dụng chính của quảng cáo @check @redact là tra cứu dữ liệu liên quan để quyết định xem có nên cho phép một số thao tác nhất định hay không, sử dụng tính năng tra cứu trong logic nhưng ẩn tính năng này khỏi ứng dụng khách. Truy vấn của bạn có thể trả về các thông báo hữu ích để xử lý chính xác trong mã ứng dụng.

Để minh hoạ, trường truy vấn sau đây sẽ kiểm tra xem người yêu cầu có vai trò "quản trị viên" thích hợp để xem những người dùng có thể chỉnh sửa phim hay không.

query GetMovieEditors($movieId: UUID!) @auth(level: USER) {
  moviePermission(key: { movieId: $movieId, userId_expr: "auth.uid" }) @redact {
    role @check(expr: "this == 'admin'", message: "You must be an admin to view all editors of a movie.")
  }
  moviePermissions(where: { movieId: { eq: $movieId }, role: { eq: "editor" } }) {
    user {
      id
      username
    }
  }
}

Để tìm hiểu thêm về các lệnh @check@redact trong quy trình kiểm tra uỷ quyền, hãy tham khảo cuộc thảo luận về việc tra cứu dữ liệu uỷ quyền.

Sử dụng @check để xác thực khoá

Một số trường đột biến, chẳng hạn như _update, có thể không hoạt động nếu không có bản ghi có khoá được chỉ định. Tương tự như vậy, các lượt tra cứu có thể trả về giá trị rỗng hoặc danh sách trống. Những lỗi này không được coi là lỗi và do đó sẽ không kích hoạt việc khôi phục.

Để phòng ngừa kết quả này, hãy kiểm thử xem có thể tìm thấy khoá bằng lệnh @check hay không.

# Delete by key, error if not found
mutation MustDeleteMovie($id: UUID!) @transaction {
  movie_delete(id: $id) @check(expr: "this != null", message: "Movie not found, therefore nothing is deleted")
}

Sử dụng liên kết response để tạo chuỗi các đột biến nhiều bước

Phương pháp cơ bản để tạo bản ghi liên quan, chẳng hạn như Movie mới và mục nhập MovieMetadata được liên kết, là:

  1. Gọi một đột biến _insert cho Movie
  2. Lưu trữ khoá được trả về của phim đã tạo
  3. Sau đó, hãy gọi một đột biến _insert thứ hai để tạo bản ghi MovieMetadata.

Nhưng với Data Connect, bạn có thể xử lý trường hợp phổ biến này trong một thao tác nhiều bước bằng cách truy cập vào kết quả của _insert đầu tiên trong _insert thứ hai.

Để tạo một ứng dụng đánh giá phim thành công, bạn cần phải làm rất nhiều việc. Hãy theo dõi danh sách việc cần làm bằng một ví dụ mới.

Sử dụng response để đặt các trường có giá trị máy chủ

Trong đột biến danh sách việc cần làm sau đây:

  • Liên kết response đại diện cho một phần đối tượng phản hồi cho đến thời điểm này, bao gồm tất cả các trường đột biến cấp cao nhất trước trường hiện tại.
  • Kết quả của thao tác todoList_insert ban đầu (trả về trường id (khoá)) sẽ được truy cập sau trong response.todoList_insert.id để chúng ta có thể chèn ngay một mục việc cần làm mới.
mutation CreateTodoListWithFirstItem(
  $listName: String!,
  $itemContent: String!
) @transaction {
  # Sub-step 1:
  todoList_insert(data: {
    id_expr: "uuidV4()", # <-- auto-generated. Or a column-level @default on `type TodoList` will also work
    name: $listName,
  })
  # Sub-step 2:
  todo_insert(data: {
    listId_expr: "response.todoList_insert.id" # <-- Grab the newly generated ID from the partial response so far.
    content: $itemContent,
  })
}

Sử dụng response để xác thực các trường bằng @check

response cũng có trong @check(expr: "..."), vì vậy, bạn có thể sử dụng response để xây dựng logic phía máy chủ phức tạp hơn. Khi kết hợp với các bước query { … } trong các đột biến, bạn có thể đạt được nhiều thành tựu hơn mà không cần thực hiện thêm bất kỳ lượt truyền dữ liệu qua lại nào giữa máy khách và máy chủ.

Trong ví dụ sau, hãy lưu ý rằng @check đã có quyền truy cập vào response.query@check luôn chạy sau bước được đính kèm.

mutation CreateTodoInNamedList(
  $listName: String!,
  $itemContent: String!
) @transaction {
  # Sub-step 1: Look up List.id by its name
  query
  @check(expr: "response.query.todoLists.size() > 0", message: "No such TodoList with the name!")
  @check(expr: "response.query.todoLists.size() < 2", message: "Ambiguous listName!") {
    todoLists(where: { name: $listName }) {
      id
    }
  }
  # Sub-step 2:
  todo_insert(data: {
    listId_expr: "response.todoLists[0].id" # <-- Now we have the parent list ID to insert to
    content: $itemContent,
  })
}

Để biết thêm thông tin về liên kết response, hãy xem tài liệu tham khảo về CEL.

Tìm hiểu các thao tác bị gián đoạn bằng @transactionquery @check

Các đột biến nhiều bước có thể gặp lỗi:

  • Các thao tác cơ sở dữ liệu có thể không thành công.
  • logic truy vấn @check có thể chấm dứt các thao tác.

Data Connect đề xuất bạn sử dụng lệnh @transaction với các đột biến nhiều bước. Điều này giúp cơ sở dữ liệu nhất quán hơn và kết quả đột biến dễ xử lý hơn trong mã ứng dụng:

  • Khi xảy ra lỗi đầu tiên hoặc @check không thành công, thao tác sẽ chấm dứt, vì vậy, bạn không cần quản lý việc thực thi bất kỳ trường nào tiếp theo hoặc đánh giá CEL.
  • Hoạt động khôi phục được thực hiện để phản hồi lỗi cơ sở dữ liệu hoặc logic @check, tạo ra trạng thái cơ sở dữ liệu nhất quán.
  • Lỗi rollback (huỷ bỏ) luôn được trả về mã ứng dụng khách.

Có thể có một số trường hợp sử dụng mà bạn chọn không sử dụng @transaction: bạn có thể chọn tính nhất quán sau cùng nếu cần lưu lượng cao hơn, khả năng mở rộng hoặc khả năng sử dụng. Tuy nhiên, bạn cần quản lý cơ sở dữ liệu và mã ứng dụng để cho phép các kết quả:

  • Nếu một trường không thực hiện được do các hoạt động cơ sở dữ liệu, thì các trường tiếp theo sẽ tiếp tục thực thi. Tuy nhiên, @check không thành công vẫn chấm dứt toàn bộ thao tác.
  • Không thực hiện các thao tác hoàn tác, nghĩa là trạng thái cơ sở dữ liệu hỗn hợp với một số cập nhật thành công và một số cập nhật không thành công.
  • Các thao tác của bạn với @check có thể cho ra kết quả không nhất quán hơn nếu logic @check sử dụng kết quả của các lượt đọc và/hoặc ghi trong bước trước.
  • Kết quả trả về cho mã ứng dụng sẽ chứa nhiều phản hồi thành công và không thành công phức tạp hơn cần được xử lý.

Giản đồ SQL tương đương

-- Movies Table
CREATE TABLE Movies (
    movie_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    release_year INT,
    genre VARCHAR(30),
    rating INT,
    description TEXT,
    tags TEXT[]
);
-- Movie Metadata Table
CREATE TABLE MovieMetadata (
    movie_id UUID REFERENCES Movies(movie_id) UNIQUE,
    director VARCHAR(255) NOT NULL,
    PRIMARY KEY (movie_id)
);
-- Actors Table
CREATE TABLE Actors (
    actor_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    name VARCHAR(30) NOT NULL
);
-- MovieActor Join Table for Many-to-Many Relationship
CREATE TABLE MovieActor (
    movie_id UUID REFERENCES Movies(movie_id),
    actor_id UUID REFERENCES Actors(actor_id),
    role VARCHAR(50) NOT NULL, # "main" or "supporting"
    PRIMARY KEY (movie_id, actor_id),
    FOREIGN KEY (movie_id) REFERENCES Movies(movie_id),
    FOREIGN KEY (actor_id) REFERENCES Actors(actor_id)
);
-- Users Table
CREATE TABLE Users (
    user_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    user_auth VARCHAR(255) NOT NULL
    username VARCHAR(30) NOT NULL
);
-- Reviews Table
CREATE TABLE Reviews (
    review_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    user_id UUID REFERENCES Users(user_id),
    movie_id UUID REFERENCES Movies(movie_id),
    rating INT,
    review_text TEXT,
    review_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE (movie_id, user_id)
    FOREIGN KEY (user_id) REFERENCES Users(user_id),
    FOREIGN KEY (movie_id) REFERENCES Movies(movie_id)
);
-- Self Join Example for Movie Sequel Relationship
ALTER TABLE Movies
ADD COLUMN sequel_to UUID REFERENCES Movies(movie_id);

Tiếp theo là gì?