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
| Field | Type |
|---|---|
| full_name | string |
| string | |
| created_at (ActiveRecord) / inserted_at (Ecto) | datetime |
| updated_at | datetime |
invoices
| Field | Type |
|---|---|
| user_id | integer |
| payment_method | string |
| paid_at | datetime |
| created_at (ActiveRecord) / inserted_at (Ecto) | datetime |
| updated_at | datetime |
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:
- 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. - 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!