Migration

Migration 是 ORM 用來紀錄資料庫表格異動的機制,隨著專案的演化,資料庫 schema 也必然需要被變更,ORM 可以紀錄每次資料庫的異動,這樣的機制就叫 migration,當代的 web 框架都有搭配各自的 ORM 也都有這樣的機制。Masonite 搭配的 ORM 是 Orator,有了 ORM,資料庫的 schema 異動都可以透過自己熟悉的框架語言去實現,不用碰 SQL 語法,…當然這只是理想的狀況,在決定欄位型態前還是得要了解後端資料庫的資料型態特性,或者是某些特殊的查詢一定要用 SQL 來寫,總之真實世界比理想狀況複雜的多。

建立 Migration 檔

情境:既有 users table,要幫每個 user 紀錄交易資料,故需要增加另一個 transactions table,並且具有關聯性。上面的描述以 ORM 的角度應該用 class 來敘述較好,但這篇文主要是談 migration 本身故不多談 class。

Craft 再次出動:

$ craft migraton create_transactions_table --create=transaction

Created migration: 2019_07_01_142116_create_transactions_table.py
  • create_transactions_table:這次 migration 的名稱而已。
  • --create=transactions:表示這次是要新建 table。

和 Rails 不同的是,craft migration 不會拿後面的參數自己去幫我們新增檔案的內容,在此後面的參數命名不會影響到產出檔案的內容,只有被明確指示的 --create=transactions 這段敘述才會被引用進檔案內容。

定義欄位

可以看到它幫我們建了一個檔案,此檔案位在 databases/migrations/ 內,內容:

from orator.migrations import Migration


class CreateTransactionsTable(Migration):

    def up(self):
        """
        Run the migrations.
        """
        with self.schema.create('transactions') as table:
            table.increments('id')
            table.timestamps()

    def down(self):
        """
        Revert the migrations.
        """
        self.schema.drop('transactions')

欄位資料型態的定義參閱 Orator 的〈Schema Builder〉文件。

把要加的欄位定義進去:

table.date('date')
table.integer('amount')
table.string('receipt_number').nullable()
table.string('description').nullable()

.nullable() 表示此欄位接受空值。

加上與 users 的外鍵欄位:

table.integer('user_id').unsigned()
table.foreign('user_id').references('id').on('users')

外鍵要求資料型態要同原本的欄位,亦即 transactions.user_idusers.id 兩者資料型態要一致。因為 users.id 有 unsigned 的屬性,所以 transactions.user_id 也必須是 unsigned 的屬性。

事實上用 craft migration 做出來定義檔都會預帶一個 id 作為主鍵的敘述,像這樣:
table.increments('id'),這邊的 increments() 都有加上 unsigned 的屬性。(好像是從 Laravel 抄來的)(其實整套都是抄 Laravel 的)

最後完整的檔案長這樣:

from orator.migrations import Migration


class CreateTransactionsTable(Migration):

    def up(self):
        """
        Run the migrations.
        """
        with self.schema.create('transactions') as table:
            table.increments('id')
            table.integer('user_id').unsigned()
            table.foreign('user_id').references('id').on('users')
            table.date('date')
            table.integer('amount')
            table.string('receipt_number').nullable()
            table.string('description').nullable()
            table.timestamps()

    def down(self):
        """
        Revert the migrations.
        """
        self.schema.drop('transactions')

跑 Migrate

定義都寫完,可以跑 migrate 了:

$ craft migrate

[OK] Migrated 2019_07_01_142116_create_transactions_table

這行指令不用進 database/migrations/ 裡面也可以跑。

最後看到 OK 就好了。

確認 Migration 狀況

多人開發的專案會遇到別人加了個 migration 自己卻沒注意到的狀況,可以用 craft migrate:status 來確認一下:

$ craft migrate:status

It took 0.33ms to execute the query ("SELECT * FROM sqlite_master WHERE type = 'table' AND name = ?", ['migrations'])
It took 0.09ms to execute the query ('SELECT "migration" FROM "migrations"', [])

+---------------------------------------------+------+
| Migration                                   | Ran? |
+---------------------------------------------+------+
| 2018_01_09_043202_create_users_table        | Yes  |
| 2019_07_01_142116_create_transactions_table | Yes  |
+---------------------------------------------+------+