2023年1月4日 星期三

AdonisJs第二天,Migration

前言

其實我猶豫很久第二天要講Route還是Migration,思慮再三還是決定從資料庫開始講起,畢竟這也是後端的根本,不過既然提到資料庫,那至少要準備一個資料庫,可以使用免錢的MariaDB,準備好那我們就開始吧。

AdonisJs中的Migration底層是用knex去實現的,可以理解為程式碼去設計資料庫的架構,並用指令及迭代去完成資料表的創建及修改。那既然提到資料庫,就不能不提及代表資料的最小單位Model,而AdonisJs的Model是用lucid去實現的,所以在Migration開始教學之前,還需要把資料庫相關的套件裝起來。


實作階段

安裝很簡單,依序下指令就好

npm i @adonisjs/lucid

node ace configure @adonisjs/lucid

這時會詢問你要使用哪一種DB,此時我們選擇我們準備好的 MySql/MariaDB (請依自行狀況調整)。

當下完上述兩個指令,db相關的cli就會跳出來了,但此時記得先到環境變數檔裡去設定資料庫連線的ip及帳號密碼,不然到時實行可是會因為找不到資料庫而報錯的!

設定完接著我們就可以開始設計資料表了

node ace make:migration [table_name]

這時在database/migrations 底下就會產生一個附有時間戳的檔案。這時間戳很重要,是電腦判斷順序的依據,有時順序錯誤就會報出錯誤。因此建議大部分時候請用指令來產生migration檔案

在AdonisJs裡面,大多數用指令產生的檔案,會依類型自動幫你複數化,假設是port => posts,唯獨model不會,產生檔案如下。


這時我們來看一下產生出來的內容

import BaseSchema from '@ioc:Adonis/Lucid/Schema'

export default class extends BaseSchema {
protected tableName = 'posts'

public async up () {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')

/**
* Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL
*/
table.timestamp('created_at', { useTz: true })
table.timestamp('updated_at', { useTz: true })
})
}

public async down () {
this.schema.dropTable(this.tableName)
}
}



tableName這應該很好懂,就是到時會產生的資料表的名子

至於up跟down這裡需稍加說明migration的執行流程,跟幾個重要指令

node ace migration:run 以下簡稱run

node ace migration:rollback 以下簡稱rollback

想像建構一個資料庫像在蓋大樓,每跑一次run就代表上一個樓層(level),所以會跑up function,一次rollback就會下一個level,所以會跑down,又因為up跟down互為相反,所以內容必須完全相反才行,例如up在新增資料表,down就必須是刪除資料表,up是修改欄位名稱,down就必須改回原本名稱。

然後每一次run系統都會把migrations資料夾底下所有檔案讀取出來,扣除掉早於上次跑run的時間點(依時間戳判斷),執行剩餘的migration。

其餘的cli都是這兩個指令的延伸,為了符合更多不同的場景,大家有空可以玩一下這裡就不贅述。

以下舉個完整的例子

今天我make:migration post,然後跑一次run,此時建立post資料表,level為1,這時我在make:migration comment,再跑一次run,此時建立comment資料表,level為2,但後來我覺得comment設計得不太好,想要刪掉重來,此時下rollback就會回到level1,comment就會被drop掉。如下圖,順序由左至右



了解整個流程,我們就可以開始設計資料表了,依照表的功能進行設計,例如post代表po文,自然有po文的人(user_id),po的標題(title),po的內容(content),也會紀錄po文的時間(created_at)或修改的時間(updated_at),也給這個po文自己的unique id (id),其他的還沒想到我們可以先不想,反正之後還可以調整,那設計出來的migration大概會是這個樣子

import BaseSchema from "@ioc:Adonis/Lucid/Schema";

export default class extends BaseSchema {
protected tableName = "posts";

public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments("id");
table.integer("user_id");
table.string("title");
table.text("content");
table.timestamp("created_at", { useTz: true });
table.timestamp("updated_at", { useTz: true });
});
}

public async down() {
this.schema.dropTable(this.tableName);
}
}



接著執行run,如果沒意外的話就恭喜你完成自己的第一張資料表。


後記

其實Migration跟Auth我也是很猶豫哪個要先做,但還是想說先把概念講一下,再開始實作可能觀眾會比較有概念,那接著Auth我們就明天見啦。(結果Route越排越後面....)

我直接在學習Migration時其實有想過,現在都什麼年代了,在功能越做越多,工程師越來越懶的情況下,為什麼不學NestJs或golang的gorm一樣,把model跟db schema同步就好,可以比較省事。

但實際自己在摸那兩套框架時測試,只要遇到欄位有時且無法轉換的情況下,其實還是會發生些不可預期的問題,要不是欄位無法轉成功,不然就是發生欄位被清空,例如原本是字串的欄位,突然被改成布林,這消失是必然的,當然這屬於比較極端的狀況,但如果發生沒有異動的狀況變成爛帳,會造成日後測試的困擾。

所以經過思考覺得還是資料庫這麼嚴謹的東西還是一步一腳印照著流程走最保險,這部分就別想偷懶了吧。


3/28更新一下

在上版到release甚至是prod之後,之後如果遇到要修改資料表就沒辦法再新建,而是要用alter的方式去修改,假設我要把title改成數字,並且新增一個欄位叫detail

export default class extends BaseSchema {
protected tableName = 'posts'

public async up() {
this.schema.table(this.tableName, (table) => {
table.integer('title').alter()
table.string('detail')
})
}

public async down() {
this.schema.table(this.tableName, (table) => {
table.string('title').alter()
      table.dropColumns('detail')
})
}
}

甚至是遇到constants的資料,要再新增完表之後直接把固定資料寫入,官方有提供一個非常方便的功能可以在migration:run完之後執行,就是defer

export default class extends BaseSchema {
protected tableName = 'tasks'

public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('code')
table.timestamps()
})

this.defer(async () => {
await Task.createMany([
         { id: 1, code: 'Test' },
         { id: 2, code: 'Code' },
     ])
})
}

public async down() {
this.schema.dropTable(this.tableName)
}
}

不過小編也遇過defer沒有跑,或是沒有跑完全的狀況(兩個檔案只跑了一個),目前原因不明,就還在觀察中。在此做個補充

沒有留言:

張貼留言