Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

ActiveRecord vs Ecto: คู่มือนักพัฒนาสำหรับ Database Abstractions - ภาค 1

ข้อมูลเป็นส่วนสำคัญของแอปพลิเคชンソฟต์แวร์ส่วนใหญ่ การ map และ query ข้อมูลจาก database เป็นงานที่นักพัฒนาต้องทำซ้ำๆ จึงสำคัญที่จะต้องเข้าใจกระบวนการและใช้ abstractions ที่ทำให้งานง่ายขึ้น

ในบทความนี้ (ภาคแรกของ 2 ภาค) คุณจะได้เห็นการเปรียบเทียบระหว่าง ActiveRecord (Ruby) และ Ecto (Elixir) เราจะดูว่าทั้งสอง tools ช่วยให้นักพัฒนาสามารถ migrate และ map database schemas ได้อย่างไร

เราจะเปรียบเทียบ "แอปเปิลกับส้ม" (Original) Batgirl ที่ไม่ต้องพูดคำเดียว กับ Batman ที่ระบุชัดเจนว่า 'I'm Batman' Implicit, convention over configuration ต่อ Explicit intention ราวน์หนึ่ง เริ่ม!

ActiveRecord

ด้วยอายุมากกว่า 10 ปีนับตั้งแต่ release โอกาสสูมที่คุณเคยได้ยิน ActiveRecord - ORM ที่มีชื่อเสียงซึ่งมาพร้อมกับ Ruby on Rails projects โดยค่าเริ่มต้น

ActiveRecord คือ M ใน MVC - model - ซึ่งเป็น layer ที่รับผิดชอบการ represent business data และ logic ActiveRecord ช่วยการสร้างและใช้ business objects ที่ข้อมูลของพวกเขาต้องการ persistent storage ใน database เป็น implementation ของ ActiveRecord pattern ซึ่งเป็น description ของ Object Relational Mapping system

แม้จะรู้จักกันดีในฐานะที่ใช้กับ Rails ActiveRecord ยังสามารถใช้เป็น standalone tool ได้ โดย embed เข้าไปใน projects อื่นๆ

Ecto

เมื่อเทียบกับ ActiveRecord Ecto เป็น tool ที่ใหม่กว่า (และขณะนี้ยังไม่ได้มีชื่อเสียงเท่าเทียม) เขียนด้วย Elixir และรวมอยู่ใน Phoenix projects โดยค่าเริ่มต้น

ต่างจาก ActiveRecord Ecto ไม่ใช่ ORM แต่เป็น library ที่ช่วยให้ใช้ Elixir เขียน queries และ interact กับ database ได้

Ecto คือ domain specific language สำหรับเขียน queries และ interact กับ databases ใน Elixir

โดย design Ecto เป็น standalone tool ถูกใช้ใน Elixir projects ต่างๆ และไม่ได้ผูกมัดกับ framework ใดๆ

เรากำลังเปรียบเทียบแอปเปิลกับส้มใช่ไหม?

ใช่เรากำลังทำอยู่! แม้ ActiveRecord และ Ecto จะต่างกันทาง semantics แต่ common features เช่น database migrations, database mappings, queries และ validations ถูก support โดยทั้งคู่ และเราสามารถบรรลุผลลัพธ์เดียวกันได้ด้วยทั้งสอง tools สำหรับผู้สนใจ Elixir ที่มาจาก Ruby background เราคิดว่านี่จะเป็นการเปรียบเทียบที่น่าสนใจ

Invoice System

ตลอดบทความจะใช้ hypothetical invoice system สำหรับ demonstration ลองจินตนาการว่าเรามีร้านขายชุดให้ซูเปอร์ हीโร่ เพื่อให้ง่าย เราจะมีแค่ 2 tables สำหรับ invoice system: users และ invoices

โครงสร้างของ tables นั้นพร้อม fields และ types ดังนี้:

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

Table users มี 4 fields: full_name, email, updated_at และ field ที่ 4 ที่ขึ้นอยู่กับ tool ที่ใช้ ActiveRecord สร้าง field created_at ในขณะที่ Ecto สร้าง inserted_at เพื่อ represent timestamp ของการ insert record ครั้งแรก

Table ที่สองชื่อ invoices มี 5 fields: user_id, payment_method, paid_at, updated_at และเหมือนกับ users table คือ created_at หรือ inserted_at ขึ้นอยู่กับ tool

Associations ระหว่าง users และ invoices tables:

  • A user has many invoices
  • An invoice belongs to a user

Migrations

Migrations ช่วยให้นักพัฒนาสามารถ evolve database schema ได้อย่างง่ายดายผ่าน iterative process ทั้ง ActiveRecord และ Ecto ช่วยให้นักพัฒนาสามารถ migrate database schema ด้วย high-level language (Ruby และ Elixir ตามลำดับ) แทนที่จะจัดการ SQL โดยตรง

มาดู migrations ใน ActiveRecord และ Ecto โดยสร้าง users และ invoices tables กัน

ActiveRecord: สร้าง Users Table

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 migrations ช่วยสร้าง tables ด้วย method create_table แม้ fields created_at และ updated_at จะไม่ได้ถูกกำหนดใน migration file การใช้ t.timestamps จะ trigger ActiveRecord ให้สร้างทั้งสอง field

โครงสร้าง Table ที่สร้างขึ้น

หลังจากรัน migration CreateUsers table ที่สร้างจะมีโครงสร้างดังนี้:

 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 ยังรับผิดชอบการสร้าง unique index สำหรับ email field Option index: {unique: true} ถูกส่งไปกับ email field definition นี่คือเหตุผลที่ table มี "index_users_on_email" UNIQUE, btree (email) index ในโครงสร้าง

Ecto: สร้าง Users Table

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 รวม functions create() และ table() เพื่อสร้าง users table ไฟล์ migration ค่อนข้างคล้ายกับ ActiveRecord ใน ActiveRecord timestamps fields (created_at และ updated_at) ถูกสร้างโดย t.timestamps ในขณะที่ Ecto timestamps fields (inserted_at และ updated_at) ถูกสร้างโดย function timestamps()

มีความแตกต่างเล็กน้อยในการสร้าง indexes ActiveRecord กำหนด index เป็น option ของ field ที่สร้าง Ecto ใช้ combination ของ functions create() และ index() เพื่อทำอย่างนั้น สอดคล้องกับวิธีที่ combination ถูกใช้สร้าง table เอง

โครงสร้าง Table ที่สร้างขึ้น

 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) 

Table ที่สร้างจากการรัน migration Financex.Repo.Migrations.CreateUsers มีโครงสร้างเหมือนกันกับ table ที่สร้างด้วย ActiveRecord

ActiveRecord: สร้าง invoices Table

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 นี้รวม method t.references ที่ไม่มีใน migration ก่อนหน้า ใช้สร้าง reference ไปที่ users table ตามที่อธิบายไว้ user has many invoices และ invoice belongs to a user Method t.references สร้าง column user_id ใน invoices table เพื่อเก็บ reference นั้น

โครงสร้าง Table ที่สร้างขึ้น

 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) 

Table ที่สร้างขึ้น follow patterns เดียวกับ table ก่อนหน้า ความแตกต่างคือ extra index (index_invoices_on_user_id) ซึ่ง ActiveRecord เพิ่มอัตโนมัติเมื่อใช้ t.references

Ecto: สร้าง invoices Table

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 ก็รองรับการสร้าง database references ด้วย function references() ต่างจาก ActiveRecord ที่ infer column name Ecto ต้องการให้นักพัฒนากำหนด column name user_id อย่างชัดเจน Function references() ก็ต้องการให้กำหนด table ที่ reference ชี้ไปอย่างชัดเจน ซึ่งในตัวอย่างนี้คือ users table

โครงสร้าง Table ที่สร้างขึ้น

 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) 

ทั้งสอง migrations ค่อนข้างคล้ายกัน สำหรับการจัดการ feature references มีความแตกต่างบ้าง:

  1. Ecto สร้าง foreign-key constraint ให้ field user_id ("invoices_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)) ซึ่ง maintain referential integrity ระหว่าง users และ invoices tables

  2. ActiveRecord สร้าง index สำหรับ column user_id อัตโนมัติ Ecto ต้องการให้นักพัฒนาระบุชัดเจน นี่คือเหตุผลที่ migration มี statement create index(:invoices, [:user_id])

ActiveRecord: Data Mapping & Associations

ActiveRecord รู้จักกับ motto "conventions over configurations" มัน infer database table names จาก model class name โดยค่าเริ่มต้น Class ชื่อ User โดยค่าเริ่มต้นใช้ table users เป็น source ActiveRecord ยัง map ทุก columns ของ table เป็น instance attribute นักพัฒนาต้องแค่กำหนด associations ระหว่าง tables ซึ่ง ActiveRecord ใช้ infer classes และ tables ที่เกี่ยวข้อง

มาดู mapping ของ users และ invoices tables ด้วย ActiveRecord:

users

class User < ApplicationRecord has_many :invoices end

invoices

class Invoice < ApplicationRecord belongs_to :user end

Ecto: Data Mapping & Associations

ในทางตรงข้าม Ecto ต้องการให้นักพัฒนาระบุชัดเจนเกี่ยวกับ data source และ fields แม้ Ecto จะมี features has_many และ belongs_to คล้ายกัน แต่ก็ต้องการให้นักพัฒนาระบุชัดเจนเกี่ยวกับ associated table และ schema module ที่ใช้ handle table schema นั้น

นี่คือวิธีที่ Ecto map users และ invoices tables:

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

สรุป

ในบทความนี้ เราเปรียบเทียบแอปเปิลกับส้มโดยไม่พอกอืด เราเปรียบเทียบว่า ActiveRecord และ Ecto จัดการ database migrations และ mapping อย่างไร การต่อสู้ของ implicit silent original Batgirl ต่อ explicit 'I'm Batman' Batman

ขอบคุณ "convention over configuration" การใช้ ActiveRecord ปกติเกี่ยวข้องกับการเขียน code น้อยกว่า Ecto ไปในทิศทางตรงข้าม ต้องการให้นักพัฒนาเป็น explicit มากขึ้นเกี่ยวกับ intentions นอกจาก "less code" ที่ดีกว่าทั่วไป ActiveRecord มี optimal defaults ที่ช่วยให้นักพัฒนาไม่ต้องตัดสินใจทุกอย่างและเข้าใจ underlying configurations ทั้งหมด สำหรับ beginners ActiveRecord เป็น solution ที่เหมาะสมกว่า เพราะมันทำ "good enough" decisions โดยค่าเริ่มต้นตราบใดที่คุณ follow standard ของมันอย่างเคร่งครัด

Explicit aspect ของ Ecto ทำให้อ่านและเข้าใจ behavior ของ code ง่ายขึ้น แต่ก็ต้องการให้นักพัฒนาเข้าใจมากขึ้นเกี่ยวกับ database properties และ features ที่มีอยู่ สิ่งที่ทำให้ Ecto ดู cumbersome ตั้งแต่แรก อาจเป็นหนึ่งใน virtues ของมัน ตามประสบการณ์ส่วนตัวในทั้ง ActiveRecord และ Ecto worlds Explicitness ของ Ecto กำจัด "behind the scene" effects และ uncertainty ที่พบได้บ่อยใน projects ที่ใช้ ActiveRecord สิ่งที่นักพัฒนาอ่านใน code คือสิ่งที่เกิดขึ้นใน application และไม่มี implicit behavior

ใน blog ที่สองในอีกไม่กี่สัปดาห์ ใน series "ActiveRecord vs Ecto" 2 ส่วน เราจะ cover ว่า queries และ validations ทำงานอย่างไรในทั้ง ActiveRecord และ Ecto

เรายินดีรู้ความคิดเห็นของคุณเกี่ยวกับบทความนี้ เรายังคงมองหา topics ใหม่ๆ ถ้าคุณมี subject ที่อยากเรียนรู้เพิ่มเติม อย่าลังเลที่จะบอกเราที่ @AppSignal!