Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

ActiveRecord vs Ecto: Hướng dẫn cho Developer về Database Abstraction – Phần 1

Dữ liệu là phần cốt lõi của hầu hết ứng dụng phần mềm. Mapping và truy vấn dữ liệu từ database là tác vụ lặp lại trong đời sống developer. Vì vậy, việc hiểu quy trình và sử dụng abstraction đơn giản hóa tác vụ này rất quan trọng.

Trong bài viết này, đầu tiên trong series 2 phần, bạn sẽ thấy so sánh giữa ActiveRecord (Ruby) và Ecto (Elixir). Chúng ta sẽ xem cả hai tool cho phép developer migrate và map database schema như thế nào.

Vậy chúng ta đang so sánh Táo và Cam. (Gốc) Batgirl, người chưa bao giờ cần nói một lời, so với Batman, rõ ràng tuyên bố "Tôi là Batman". Implicit, convention over configuration, so với Explicit intention. Round one. Fight!

ActiveRecord

Với hơn 10 năm kể từ khi ra mắt, khả năng cao bạn đã nghe về ActiveRecord - ORM nổi tiếng đi kèm mặc định với dự án Ruby on Rails.

ActiveRecord là M trong MVC - model - layer chịu trách nhiệm đại diện cho business data và logic. ActiveRecord hỗ trợ tạo và sử dụng business object có dữ liệu cần lưu trữ bền vững trong database. Nó là implementation của ActiveRecord pattern - mô tả của hệ thống Object Relational Mapping.

Mặc dù nổi tiếng dùng với Rails, ActiveRecord cũng có thể dùng standalone, nhúng vào project khác.

Ecto

So với ActiveRecord, Ecto là tool khá mới (và lúc này chưa nổi tiếng bằng). Nó viết bằng Elixir và đi kèm mặc định trong project Phoenix.

Khác với ActiveRecord, Ecto không phải ORM, mà là library cho phép dùng Elixir viết query và tương tác database.

Ecto là domain specific language để viết query và tương tác database trong Elixir.

Theo thiết kế, Ecto là tool standalone, dùng trong các project Elixir khác nhau và không gắn liền framework nào.

Các bạn Có Đang So Sánh Táo Và Cam?

Đúng rồi! Mặc dù ActiveRecord và Ecto khác nhau về mặt ngữ nghĩa, nhưng các tính năng chung như database migration, database mapping, query và validation đều được hỗ trợ bởi cả hai. Và ta có thể đạt được cùng kết quả dùng cả hai tool. Dành cho những ai quan tâm Elixir từ background Ruby, chúng tôi nghĩ đây là so sánh thú vị.

Hệ thống Invoice (Hóa đơn)

Trong suốt bài viết, một hệ thống invoice giả định sẽ dùng để demo. Hãy tưởng tượng cửa hàng bán vest cho siêu anh hùng. Để đơn giản, ta chỉ có 2 bảng: users và invoices.

Dưới đây là cấu trúc 2 bảng với field và type:

users

FieldType
full_namestring
emailstring
created_at (ActiveRecord) / inserted_at (Ecto)datetime
updated_atdatetime

invoices

FieldType
user_idinteger
payment_methodstring
paid_atdatetime
created_at (ActiveRecord) / inserted_at (Ecto)datetime
updated_atdatetime

Bảng users có 4 field: full_name, email, updated_at và field thứ 4 phụ thuộc tool. ActiveRecord tạo created_at trong khi Ecto tạo inserted_at để biểu diễn timestamp khi record đầu tiên được insert.

Bảng thứ hai là invoices. Có 5 field: user_id, payment_method, paid_at, updated_at và tương tự created_at hoặc inserted_at tùy tool.

Mối quan hệ giữa users và invoices:

  • User has many invoices (User có nhiều invoice)
  • Invoice belongs to user (Invoice thuộc về user)

Migrations

Migration cho phép developer dễ dàng phát triển database schema theo thời gian, dùng quy trình lặp. Cả ActiveRecord và Ecto cho phép migrate schema dùng high-level language (Ruby và Elixir) thay vì trực tiếp SQL.

Hãy xem migration hoạt động như thế nào trong ActiveRecord và Ecto bằng cách tạo 2 bảng users và invoices.

ActiveRecord: Tạo bảng Users

Migration

class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :full_name, null: false
      t.string :email, index: {unique: true}, null: false
      t.timestamps
    end
  end
end

ActiveRecord migration tạo bảng dùng phương thức create_table. Mặc dù created_at và updated_at không định nghĩa trong file migration, dùng t.timestamps kích hoạt ActiveRecord tạo cả hai.

Cấu trúc bảng tạo ra

Sau khi chạy migration CreateUsers, bảng tạo ra có cấu trúc:

 Column | Type | Nullable | Default
------------+-----------------------------+----------+-----------------------------------
 id         | bigint | not null | nextval('users_id_seq'::regclass)
 full_name  | character varying | not null |
 email      | character varying | not null |
 created_at | timestamp without time zone | not null |
 updated_at | timestamp without time zone | not null |
 Indexes:
 "users_pkey" PRIMARY KEY, btree (id)
 "index_users_on_email" UNIQUE, btree (email)

Migration cũng tạo unique index cho field email. Option index: {unique: true} truyền cho định nghĩa email. Đó là lý do bảng có index "index_users_on_email" UNIQUE, btree (email).

Ecto: Tạo bảng Users

Migration

defmodule Financex.Repo.Migrations.CreateUsers do
  use Ecto.Migration
  def change do
    create table(:users) do
      add :full_name, :string, null: false
      add :email, :string, null: false
      timestamps()
    end
    create index(:users, [:email], unique: true)
  end
end

Ecto migration kết hợp hàm create() và table() để tạo bảng users. File migration Ecto khá giống ActiveRecord. Trong ActiveRecord, timestamp fields (created_at, updated_at) tạo bởi t.timestamps; trong Ecto tạo bởi timestamps() (inserted_at, updated_at).

Có chút khác biệt về tạo index. ActiveRecord định nghĩa index như option của field. Ecto dùng kết hợp create() và index(), nhất quán với cách tạo bảng.

Cấu trúc bảng tạo ra

 Column     | Type | Nullable | Default
--------------+-----------------------------+----------+-----------------------------------
 id           | bigint | not null | nextval('users_id_seq'::regclass)
 full_name    | character varying(255) | not null |
 email        | character varying(255) | not null |
 inserted_at  | timestamp without time zone | not null |
 updated_at   | timestamp without time zone | not null |
 Indexes:
 "users_pkey" PRIMARY KEY, btree (id)
 "users_email_index" UNIQUE, btree (email)

Bảng tạo ra từ migration Financex.Repo.Migrations.CreateUsers có cấu trúc giống hệt ActiveRecord.

ActiveRecord: Tạo bảng invoices

Migration

class CreateInvoices < ActiveRecord::Migration[5.2]
  def change
    create_table :invoices do |t|
      t.references :user
      t.string :payment_method
      t.datetime :paid_at
      t.timestamps
    end
  end
end

Migration này có t.references chưa có ở trước. Dùng để tạo reference đến bảng users. Như mô tả, user has many invoices, invoice belongs to user. t.references tạo cột user_id trong bảng invoices lưu reference đó.

Cấu trúc bảng tạo ra

 Column       | Type | Nullable | Default
----------------+-----------------------------+----------+--------------------------------------
 id             | bigint | not null | nextval('invoices_id_seq'::regclass)
 user_id        | bigint |          |
 payment_method | character varying |          |
 paid_at        | timestamp without time zone |          |
 created_at     | timestamp without time zone | not null |
 updated_at     | timestamp without time zone | not null |
 Indexes:
 "invoices_pkey" PRIMARY KEY, btree (id)
 "index_invoices_on_user_id" btree (user_id)

Bảng tạo ra theo cùng pattern. Khác biệt duy nhất là index thêm index_invoices_on_user_id, ActiveRecord tự thêm khi dùng t.references.

Ecto: Tạo bảng invoices

Migration

defmodule Financex.Repo.Migrations.CreateInvoices do
  use Ecto.Migration
  def change do
    create table(:invoices) do
      add :user_id, references(:users)
      add :payment_method, :string
      add :paid_at, :utc_datetime
      timestamps()
    end
    create index(:invoices, [:user_id])
  end
end

Ecto cũng hỗ trợ tạo database reference bằng hàm references(). Khác ActiveRecord tự suy ra tên cột, Ecto yêu cầu developer định nghĩa rõ user_id. Hàm references() cũng yêu cầu định nghĩa rõ bảng tham chiếu (ví dụ: users).

Cấu trúc bảng tạo ra

 Column       | Type | Nullable | Default
----------------+-----------------------------+----------+--------------------------------------
 id             | bigint | not null | nextval('invoices_id_seq'::regclass)
 user_id        | bigint |          |
 payment_method | character varying(255) |          |
 paid_at        | timestamp without time zone |          |
 inserted_at    | timestamp without time zone | not null |
 updated_at     | timestamp without time zone | not null |
 Indexes:
 "invoices_pkey" PRIMARY KEY, btree (id)
 "invoices_user_id_index" btree (user_id)
 Foreign-key constraints:
 "invoices_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)

Cả 2 migration khá giống. Về cách handle references, có vài khác biệt:

  1. Ecto tạo foreign-key constraint cho user_id ("invoices_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)), duy trì referential integrity giữa users và invoices.
  2. ActiveRecord tự tạo index cho cột user_id. Ecto yêu cầu developer explicit về điều này. Đó là lý do migration có create index(:invoices, [:user_id]).

ActiveRecord: Data Mapping & Associations

ActiveRecord nổi tiếng với motto "conventions over configurations". Nó suy ra tên bảng database từ tên class model, mặc định. Class User mặc định dùng bảng users. ActiveRecord cũng map tất cả cột bảng thành instance attribute. Developer chỉ cần định nghĩa association giữa các bảng. Chúng cũng dùng để ActiveRecord suy ra class và bảng liên quan.

Xem cách map 2 bảng users, invoices bằng ActiveRecord:

users

class User < ApplicationRecord
  has_many :invoices
end

invoices

class Invoice < ApplicationRecord
  belongs_to :user
end

Ecto: Data Mapping & Associations

Ngược lại, Ecto yêu cầu developer explicit về data source và field. Mặc dù Ecto có has_many và belongs_to tương tự, nhưng cũng yêu cầu explicit về associated table và schema module xử lý schema bảng đó.

Cách Ecto map 2 bảng:

users

defmodule Financex.Accounts.User do
  use Ecto.Schema
  schema "users" do
    field :full_name, :string
    field :email, :string
    has_many :invoices, Financex.Accounts.Invoice
    timestamps()
  end
end

invoices

defmodule Financex.Accounts.Invoice do
  use Ecto.Schema
  schema "invoices" do
    field :payment_method, :string
    field :paid_at, :utc_datetime
    belongs_to :user, Financex.Accounts.User
    timestamps()
  end
end

Tổng kết

Trong bài này, chúng ta so sánh táo và cam không chớp mắt. So sánh cách ActiveRecord và Ecto handle migration và mapping. Trận chiến giữa implicit silent Batgirl gốc và explicit "I'm Batman" Batman.

Nhờ "convention over configuration", ActiveRecord thường ít code hơn. Ecto đi ngược chiều, yêu cầu developer explicit hơn về ý định. Ngoài "less code" nói chung tốt hơn, ActiveRecord có optimal defaults giúp developer không phải quyết định mọi thứ và hiểu tất cả underlying config. Cho beginner, ActiveRecord phù hợp hơn, vì nó quyết định "good enough" mặc định miễn là bạn tuân thủ standard của nó.

Sự explicit của Ecto giúp dễ đọc hiểu behavior của code, nhưng cũng yêu cầu developer hiểu hơn về database property và feature. Điều làm Ecto trông cumbersome lúc đầu, thực ra là đức tính của nó. Dựa trên kinh nghiệm cả hai, explicitness của Ecto loại bỏ "behind the scene" effects và uncertainty thường thấy project ActiveRecord. Developer đọc code là thấy điều xảy ra trong app, không có implicit behavior.

Trong blog phần 2 vài tuần sau, trong series "ActiveRecord vs Ecto" 2 phần, chúng ta sẽ cover query và validation trong cả hai.

Chúng tôi rất muốn biết ý kiến bạn về bài này. Luôn tìm topic mới, nếu bạn có chủ đề muốn tìm hiểu thêm, đừng ngần ngại cho chúng tôi biết @AppSignal!