前幾天開始接觸 PostgreSQL,也透過 SQL 學會了如何新增、查詢、修改和刪除資料,到了這裡,已經可以直接透過 SQL 和資料庫溝通,例如使用 SELECT 查詢資料、INSERT 新增資料、UPDATE 修改資料,以及 DELETE 刪除資料。
這種方式非常直接,因為 SQL 本身就是資料庫理解的語言,不過當專案逐漸變大,資料表和資料存取邏輯也會跟著增加,除了 SQL 查詢本身,還需要處理資料庫連線、參數、查詢結果,以及不同資料表之間的關係,這些工作累積之後,資料存取程式碼也會越來越多。
因此在實務開發中,除了直接使用 SQL,也常會透過 ORM 來處理資料庫操作,ORM 並不是取代 SQL,而是在應用程式與資料庫之間增加一層抽象,讓開發者可以用更接近程式資料模型的方式操作資料庫。
ORM 是 Object-Relational Mapping,也就是「物件與關聯式資料庫之間的對應」。
這裡要先區分兩個容易混淆的名稱:ORM 是一種技術概念,而 TypeORM 是實際實作這個概念的工具。
ORM 描述的是一種讓程式中的物件與關聯式資料庫建立對應,並透過程式碼操作資料庫的方式,至於實際使用哪個工具,則可以有不同選擇。
這篇先使用 TypeORM 來實際操作 PostgreSQL,所以可以先把兩者理解成:
ORM
↓
資料庫操作的技術概念
TypeORM
↓
實作 ORM 概念的工具
因此看到 ORM 時,指的是這一整類的技術概念,看到 TypeORM 時,則是指這次實際使用的套件。
在 JavaScript 裡,我們習慣操作的是物件,例如一筆 Note 可以表示成:
const note = {
id: 1,
title: '學習 ORM',
content: '開始理解 ORM 的用途'
};
但在 PostgreSQL 裡,資料會儲存在資料表中:
notes
--------------------------------
id | title | content
1 | 學習 ORM | 開始理解 ORM 的用途
ORM 做的事情,就是建立程式裡的物件與資料庫資料表之間的對應,讓我們可以用程式的資料模型來操作資料。
例如原本直接使用 SQL 查詢所有 notes:
SELECT * FROM notes;
透過 TypeORM 可以寫成:
const notes = await noteRepository.find();
這裡的差別不是 PostgreSQL 突然開始理解 JavaScript,而是 TypeORM 在中間處理了應用程式與資料庫之間的轉換。
程式端操作的是 Repository,底層仍然會產生資料庫可以執行的 SQL,再交給 PostgreSQL 處理。整體流程可以理解成:
Node.js / Express
↓
TypeORM
↓
SQL
↓
PostgreSQL
所以 ORM 的角色,就是讓應用程式不必每一次都直接處理資料庫底層的細節。
要讓 ORM 知道程式中的資料模型和資料庫怎麼對應,首先需要描述資料結構,在 TypeORM 裡,這個角色就是 Entity。
假設 PostgreSQL 有一張 notes 資料表:
notes
├── id
├── title
├── content
└── created_at
可以在 TypeORM 裡建立對應的 Entity:
const { EntitySchema } = require('typeorm');
const Note = new EntitySchema({
name: 'Note',
tableName: 'notes',
columns: {
id: {
type: Number,
primary: true,
generated: true
},
title: {
type: String
},
content: {
type: String
}
}
});
module.exports = Note;
這裡的 Note 就是 Entity,它描述 notes 這張資料表,以及資料表中的欄位。因此可以建立這樣的對應:
資料庫 程式
users ←→ User
notes ←→ Note
comments ←→ Comment
不同 ORM 或框架可能會使用 Model、Entity 等不同名稱,但目前可以先掌握這個概念:Entity 是用來描述資料庫資料結構,並建立程式模型與資料表之間對應的方式。
有了 Entity 之後,還需要透過 Repository 進行實際的資料庫操作。可以先把兩者理解成:
Note Entity
↓
描述資料
Note Repository
↓
操作資料
PostgreSQL
也就是 Entity 負責「這筆資料長什麼樣子」,Repository 負責「怎麼對這筆資料進行查詢、新增、修改與刪除」。
ORM 和 Raw SQL 並不是互相取代,而是不同層次的操作方式。
Raw SQL 直接描述資料庫要執行的查詢,因此對資料庫的控制比較直接。遇到複雜的 JOIN、GROUP BY、Aggregation,或需要使用 PostgreSQL 特有功能時,直接撰寫 SQL 往往會更容易掌握。
ORM 則適合處理大量常見的資料存取,例如 CRUD,這些操作通常結構相似,交給 ORM 後可以用比較接近資料模型的方式處理,也能減少重複的資料庫程式碼。
因此實際開發時,兩者通常會搭配:
一般的 CRUD
→ ORM
複雜的查詢
→ QueryBuilder / Raw SQL
所以使用 ORM 並不代表之後完全不寫 SQL,而是把常見的資料庫操作交給較高階的抽象處理,需要細緻控制時,再回到更接近資料庫的 SQL。
到了這裡,直接把 Day 16 學過的 SQL 和 TypeORM 放在一起比較,會最容易理解兩種方式的差異。
假設目前有一張 notes 資料表:
notes
--------------------------------
id
title
content
created_at
Day 16 使用 SQL:
SELECT * FROM notes;
使用 TypeORM:
const notes = await noteRepository.find();
這裡 find() 代表取得這個 Repository 對應的資料。
SQL:
SELECT *
FROM notes
WHERE id = 1;
TypeORM:
const note = await noteRepository.findOneBy({
id: 1
});
這段程式是使用條件找到指定的 Note,概念上對應到 SQL 裡的 WHERE id = 1。
SQL:
INSERT INTO notes (title, content)
VALUES ('學習 ORM', '開始理解 ORM');
TypeORM:
const note = noteRepository.create({
title: '學習 ORM',
content: '開始理解 ORM'
});
const savedNote = await noteRepository.save(note);
這裡的 create() 會建立 Entity,save() 則負責把這個 Entity 儲存到資料庫。save() 不應該單純理解成 INSERT 的另一種寫法,因為它除了新增資料,也可以處理更新既有 Entity 的情況。
SQL:
UPDATE notes
SET title = '開始學習 ORM'
WHERE id = 1;
TypeORM:
await noteRepository.update(
1,
{
title: '開始學習 ORM'
}
);
這裡透過 update() 指定要修改的資料,以及需要更新的欄位。
SQL:
DELETE FROM notes
WHERE id = 1;
TypeORM:
await noteRepository.delete(1);
這裡直接指定要刪除的資料 ID。
從這組對照可以看到,使用 SQL 時,我們會直接描述資料庫要執行的操作;使用 ORM 後,則會透過 Repository 和 Entity 來描述資料模型與資料存取行為。
這並不是把 SELECT、INSERT、UPDATE、DELETE 一一換成 find()、save()、update()、delete() 而已,而是把操作的思考層次從 SQL 往資料模型移動。
雖然使用 TypeORM 後,程式碼裡不一定會直接出現 SQL,但底層仍然需要透過 SQL 與 PostgreSQL 溝通。
例如:
const notes = await noteRepository.find();
實際上會經過 ORM 產生查詢,再交給 PostgreSQL 執行,最後將結果整理成程式可以使用的資料。
noteRepository.find()
↓
TypeORM
↓
SQL
↓
PostgreSQL
↓
Query Result
↓
TypeORM
↓
JavaScript Object
因此 SQL 仍然很重要。前面先學 SQL,就是為了知道資料庫實際在做什麼;現在開始接觸 ORM,則是進一步理解應用程式可以怎麼更高階地管理這些資料庫操作。
Day 16 學的是如何直接透過 SQL 操作資料庫,Day 17 則是在這個基礎上往上一層,開始理解 ORM 如何把資料庫操作整合進程式架構。
這裡最重要的是分清楚 ORM 和 TypeORM 並不是同一件事情。ORM 是一種讓程式模型與關聯式資料庫建立對應的技術概念,TypeORM 則是這次拿來實作 ORM 的工具。
透過 TypeORM,可以用 Entity 描述資料結構,再透過 Repository 操作資料。對於常見的 CRUD,可以不用每次都自己撰寫完整 SQL,而是用更接近資料模型的方式處理。
不過 ORM 並沒有改變資料庫本身的運作方式,底層仍然是 SQL:
Node.js / Express
↓
TypeORM
↓
SQL
↓
PostgreSQL
因此前面學 SQL 並不是為了之後改用 ORM 就丟掉,而是讓自己知道 ORM 背後到底在做什麼。
到了這裡,除了會操作資料,也開始碰到另一個需要管理的問題:資料庫的結構本身會隨著專案一起變化。
例如未來 notes 新增 status 欄位,就不只是修改 Entity,實際的 PostgreSQL 資料表也需要同步調整,而且還要確保不同環境使用的是相同的資料庫結構。
下一篇,就會進入 Migration,開始管理資料庫結構的變更。