iT邦幫忙

2023 iThome 鐵人賽

0
自我挑戰組

用ChatGPT詠唱來完成工作與點亮前後端技能樹系列 第 50

【Day50】ChatGPT請教教我:NestJS!(九)- Knex!transaction!migration!

  • 分享至 

  • xImage
  •  

我們NestJS系列主題最後一哩路~Knex進階內容
這邊要討論幾個上一篇提到的進階主題

  1. transaction
  2. 模組化查詢
  3. migration
  4. 其他可以做migration 的db工具與優缺點比較

transaction(交易)

https://ithelp.ithome.com.tw/upload/images/20231103/20135750el1CJgGiRh.png

當然可以!首先,讓我來詳細介紹一下Knex中的Transactions機制。

在Knex中,Transactions允許你執行一系列的資料庫操作,並且保證這些操作要麼全部成功,要麼全部失敗。這在處理需要保證資料完整性的情況下非常有用,比如在新增、更新或刪除多筆資料時。

以下是在Knex中使用Transactions的步驟:

  1. 開始Transaction

    要開始一個Transaction,你需要使用transaction方法。這個方法會返回一個Promise,你可以在這個Promise中進行你的資料庫操作。

    import { transaction } from 'knex';
    
    const executeTransaction = async () => {
      await knex.transaction(async (trx) => {
        // 在這裡進行資料庫操作
      });
    };
    
  2. 執行資料庫操作

    在Transaction內部,你可以使用trx物件來執行資料庫操作,比如插入、更新或刪除資料。

    const executeTransaction = async () => {
      await knex.transaction(async (trx) => {
        await trx('users').insert({ name: 'John Doe' });
        await trx('posts').update({ title: 'New Title' }).where({ user_id: 1 });
      });
    };
    
  3. 確認Transaction

    如果所有的操作都成功完成,Transaction會自動提交。如果有任何一個操作失敗,Transaction會自動回滾,所有的操作都會被撤銷。

  4. 處理錯誤

    如果在Transaction中的任何一個操作中拋出了錯誤,Transaction會自動回滾。你可以使用try-catch來處理錯誤。

    const executeTransaction = async () => {
      try {
        await knex.transaction(async (trx) => {
          // ...
        });
      } catch (error) {
        console.error('Transaction failed:', error);
      }
    };
    
  5. 結束Transaction

    Transaction會在所有操作完成後自動提交或回滾,你不需要手動結束它。

這就是在Knex中使用Transactions的基本步驟。請記得在使用Transactions時要小心處理錯誤,以確保資料的完整性。

如果你有任何其他的問題或需要進一步的說明,請隨時告訴我!

範例中的knex 就是替換成我們上個章節處理好的連線 this.knexService.db 的那個東西
因為是新的對話,所以ChatGPT不清楚我們的設置,以下就自行替換連線語法

看了以上內容,我們現在不知道使用時機以及他的運作邏輯是什麼、如何達成他所說的回滾
繼續追問!
https://ithelp.ithome.com.tw/upload/images/20231103/20135750ZEplxG4DdL.png

Transactions通常用在需要保證一系列資料庫操作要麼全部成功,要麼全部失敗的情況下。以下是一些常見的使用情境:

  1. 保證資料完整性:當你需要在多個表中進行操作,並且要保證這些操作要麼全部成功,要麼全部失敗時,可以使用Transactions。比如在新增一筆訂單時,你可能需要同時更新庫存和記錄交易紀錄。
  2. 防止競態條件:當多個使用者同時對同一筆資料進行操作時,可能會發生競態條件(race condition)的情況。使用Transactions可以確保在一個Transaction中的操作是原子的,不會被其他使用者干擾。
  3. 批量操作:當你需要執行一系列相關的資料庫操作時,可以使用Transactions來保證這些操作的完整性。

現在讓我來詳細說明Knex對於Transactions功能的實作原理:

在Knex中,當你開始一個Transaction時,它會在資料庫中啟動一個隔離層,稱為Transaction隔離。這個隔離層會追蹤所有在Transaction內進行的資料庫操作,並且在Transaction提交或回滾時確保這些操作的完整性。

Knex使用資料庫引擎(如MySQL、PostgreSQL等)提供的Transaction特性來實現這個功能。具體來說,它使用了資料庫引擎的BEGIN TRANSACTIONCOMMITROLLBACK等指令來控制Transaction的開始、提交和回滾。

當你在Transaction中執行操作時,Knex會將這些操作包裝在一個事務中,並且在事務結束時使用COMMIT指令提交,或者在發生錯誤時使用ROLLBACK指令回滾。

這樣,就保證了在一個Transaction中的所有操作要麼全部成功,要麼全部失敗。

希望這樣的解釋對你有幫助!如果你有任何其他的問題,請隨時告訴我。

ChatGPT所說的情況,我自己在工作上也都有遇到
第一點&第三點比較容易懂,多個或單個表格中做了一系列的更改操作
為了防範中途出現問題,造成有些指令有執行,而有些沒有
所以使用一個Transaction ,確保全部執行,全是全部不執行

這邊針對第二點稍微說明一下
在我工作上,有一個資料表中會有一個「任務清單」,裡面的資料列會代表著一件一件的任務
清單內有欄位表示ID、目前狀態、正在執行這個任務的使用者ID(可能為空)

多個使用者都會頻繁地select這個任務清單,
當有任務需要處理時,就按下按鈕發送請求,希望從任務清單中分配一件任務到自己手上

所以發送請求之後,伺服器做的事情是這樣

  1. 找一筆待處理任務的ID
  2. 把該筆ID的狀態與使用者變更成自己處理中

這兩個動作的query是分開丟給SQL的
就會導致也許有多個使用者在執行步驟一:找一筆待處理任務的ID時
取得一樣的ID,接著第二步就會有人變更成功,有人變更失敗

同樣的情況可能會發生在購買庫存貨、訂票,或… YOASOBI演唱會售票(X)
為什麼Transaction 可以防止這樣的事情發生呢?

我們請ChatGPT解釋說明

Transactions 與 競態條件(race condition)

https://ithelp.ithome.com.tw/upload/images/20231103/20135750RSDGRPAMBt.png

當你使用Transactions來防止競態條件時,你通常是在多個使用者或多個執行緒之間進行操作,以確保一個操作完成後,下一個操作才能開始。這是非常重要的,因為如果多個使用者同時對資料進行操作,可能會導致不一致的資料狀態。

以下是一個情境示範,假設你有一個資料庫表格用於儲存產品庫存,並且多個使用者想要同時更新這個庫存:

import { transaction } from 'knex';

const updateProductStock = async (productId, quantityToDeduct) => {
  try {
    // 開始一個Transaction
    await knex.transaction(async (trx) => {
      // 先查詢當前庫存數量
      const currentStock = await trx('products').select('stock').where('id', productId).first();

      if (currentStock) {
        // 檢查是否庫存足夠
        if (currentStock.stock >= quantityToDeduct) {
          // 扣減庫存
          await trx('products').where('id', productId).decrement('stock', quantityToDeduct);
        } else {
          throw new Error('庫存不足');
        }
      } else {
        throw new Error('找不到產品');
      }
    });

    console.log('庫存更新成功');
  } catch (error) {
    console.error('庫存更新失敗:', error.message);
  }
};

在這個示範中,我們使用Transactions來保護對庫存的更新操作。當一個使用者想要扣減庫存時,我們首先查詢當前庫存,然後檢查是否庫存足夠。如果庫存足夠,我們扣減庫存,然後在Transaction中提交這個操作。

這樣,當多個使用者同時嘗試更新庫存時,Transactions確保這些操作是原子的,不會交錯進行,從而防止競態條件的發生。如果庫存不足或找不到產品,Transaction會回滾,確保資料的完整性。

這是一個簡單的情境示範,但Transactions可以在更複雜的應用中用於處理競態條件,確保資料的一致性。

https://ithelp.ithome.com.tw/upload/images/20231103/20135750StpTkQUf8N.png

Transactions在防止兩個請求同時完成「檢查是否庫存足夠」的步驟的過程中,利用了資料庫引擎的隔離層(isolation level)和鎖定機制。

具體來說,當你在一個Transaction中進行資料庫操作時,資料庫會將相關的資料行或表格進行鎖定,以防止其他使用者對這些資料進行操作。這樣可以確保在這個Transaction中進行的操作不會被其他使用者干擾。

在我們的情境示範中,當一個使用者嘗試扣減庫存時,首先會進行一個查詢操作,以獲取當前的庫存數量。這個查詢操作會在Transaction內部進行,而且在這個Transaction執行期間,相關的資料行或表格會被鎖定。

這意味著在這個Transaction執行期間,其他使用者如果也想要進行與這筆庫存相關的操作(比如更新、扣減),他們將會被阻止,直到這個Transaction完成。

這樣,就保證了在這個Transaction中的「檢查是否庫存足夠」步驟能夠獲得正確的庫存數量,而不會受到其他使用者的干擾。

總結來說,Transactions利用了資料庫引擎的隔離層和鎖定機制,確保在一個Transaction中的操作是原子的,不會被其他使用者干擾,從而防止競態條件的發生。

所以可以想像成是在櫃檯點餐,一個人確認完庫存,並點完餐之後,才會輪到下一個人確認庫存
在Transaction內的操作,都必須完成之後,才會做下一個Transaction

模組化查詢

接下來一起來了解一下ChatGPT所說的模組化查詢是什麼
這個名詞我也第一次聽到,所以也是上一篇才臨時加入大綱內的
https://ithelp.ithome.com.tw/upload/images/20231103/20135750ptfDakw517.png

當然可以!讓我來為你詳細介紹如何在Nest專案中使用Knex進行模組化查詢。

首先,確保你已經在Nest專案中安裝了Knex和相應的資料庫驅動程式(比如pgmysql2等)。

  1. 安裝 Knex

    使用以下指令在你的Nest專案中安裝Knex:

    npm install knex
    
  2. 建立 Knex 連線

    在Nest專案中,你可以在src目錄下建立一個knex資料夾,並在其中建立一個knex.ts文件來配置Knex連線。

    // src/knex/knex.ts
    
    import * as knex from 'knex';
    
    const config = {
      client: 'pg', // 根據你使用的資料庫選擇相應的client
      connection: {
        host: 'localhost',
        user: 'your_username',
        password: 'your_password',
        database: 'your_database',
      },
    };
    
    const knexInstance = knex(config);
    
    export default knexInstance;
    
  3. 建立模組化查詢

    在Nest中,你可以根據你的需求建立不同的模組,每個模組都可以包含相應的Knex查詢。

    例如,如果你想要建立一個與使用者相關的模組,可以進行如下操作:

    • 創建一個名為users的資料夾,在其中包含users.module.tsusers.service.ts文件。

    • users.service.ts中,你可以使用之前建立的Knex連線,並實現相關的查詢操作:

      // src/users/users.service.ts
      
      import { Injectable } from '@nestjs/common';
      import knex from '../knex/knex';
      
      @Injectable()
      export class UsersService {
        async getAllUsers() {
          return await knex('users').select('*');
        }
      
        async getUserById(id: number) {
          return await knex('users').where('id', id).first();
        }
      
        async createUser(user: any) {
          return await knex('users').insert(user).returning('*');
        }
      
        async updateUser(id: number, user: any) {
          return await knex('users').where('id', id).update(user).returning('*');
        }
      
        async deleteUser(id: number) {
          return await knex('users').where('id', id).del();
        }
      }
      
  4. 將模組加入 Nest 中

    users.module.ts文件中,將UsersService加入到Nest模組中。

    // src/users/users.module.ts
    
    import { Module } from '@nestjs/common';
    import { UsersService } from './users.service';
    
    @Module({
      providers: [UsersService],
      exports: [UsersService],
    })
    export class UsersModule {}
    
  5. 在 Controller 中使用模組

    最後,在你的Nest Controller 中使用剛剛建立的UsersService,以處理相應的HTTP請求。

    // src/users/users.controller.ts
    
    import { Controller, Get, Param } from '@nestjs/common';
    import { UsersService } from './users.service';
    
    @Controller('users')
    export class UsersController {
      constructor(private readonly usersService: UsersService) {}
    
      @Get()
      getAllUsers() {
        return this.usersService.getAllUsers();
      }
    
      @Get(':id')
      getUserById(@Param('id') id: string) {
        return this.usersService.getUserById(Number(id));
      }
    }
    

這樣,你就建立了一個模組化的Knex查詢在Nest專案中。你可以根據需要在UsersService中添加其他查詢,並在Controller中使用它們來處理相應的請求。

希望這個教學對你有幫助!如果你有任何其他的問題,請隨時告訴我。

所以模組化有點像是先把一個Query指令存放在一個function中
概念我覺得有點像是資料庫的預儲程序那樣,將指令包裝起來以便重複使用
後續就是直接呼叫這個預先定義好的function,來進行query

來和ChatGPT確認一下什麼時候會需要「模組化查詢」,目的、優缺點各別是什麼
https://ithelp.ithome.com.tw/upload/images/20231103/20135750VQBhXKSqtY.png

使用模組化查詢是為了更有效地組織和管理資料庫操作,尤其是在大型或複雜的應用程序中。以下是選擇使用模組化查詢的情況、目的以及其優點和缺點:

情況:

  1. 大型應用程序:當你的應用程序變得複雜,包含許多不同的資料庫操作時,模組化查詢可以幫助你組織和管理這些操作。
  2. 多個模塊:如果你的應用程序分為多個模塊,每個模塊需要進行不同的資料庫操作,你可以為每個模塊創建獨立的模組化查詢。
  3. 提高代碼重用性:模組化查詢可以被多個部分重複使用,減少代碼重複並提高代碼的可維護性。
  4. 降低耦合度:模組化查詢使得你可以將資料庫操作從控制器和服務中分離出來,降低它們之間的耦合度。

目的:

  1. 組織和管理資料庫操作:模組化查詢可以幫助你將相關的資料庫操作組織在一起,使代碼更易於維護。
  2. 提高代碼可讀性:通過將相關操作分組在一個模組中,可以使代碼更易讀和理解。
  3. 促進代碼重用:模組化查詢可以在應用程序的不同部分之間共享,提高代碼的重用性。

優點:

  1. 組織性:模組化查詢可以幫助你清晰地組織和管理資料庫操作,使代碼結構更加清晰。
  2. 可維護性:通過將相關操作放在一個模組中,可以更容易地進行修改、維護和測試。
  3. 代碼重用:可以在不同的部分中重複使用模組化查詢,減少重複代碼的數量。

缺點:

  1. 過度抽象:如果過度使用模組化查詢,可能會導致過度抽象和過度分層,使得代碼變得難以理解。
  2. 性能影響:過多的模組化查詢可能會增加函數調用的次數,導致較小的性能影響。

綜合來說,模組化查詢是一種有助於組織和管理資料庫操作的有效方法,特別適用於大型或複雜的應用程序。然而,需要注意避免過度抽象和過度使用,以保持代碼的清晰度和可讀性。

.fn.now()

這邊插播一個上一篇忘記討論的
上一篇提到如果要使用像是mysql的 now() 這樣的內建函數
必須使用.raw() 來寫原始SQL語法語句,然後執行

但會用到 now() 的機率其實不算低
來問一下ChatGPT有沒有其他方式可以達成
https://ithelp.ithome.com.tw/upload/images/20231103/20135750kqQPaxwFj8.png

除了使用.raw()方法之外,Knex還提供了另一種方法來使用SQL內建函數,那就是使用.fn()方法。

使用 .fn() 方法

.fn()方法允許你使用SQL內建函數,並且提供了更清晰的方式來將它們整合到你的查詢中。

例如,如果你想在Knex查詢中使用SQL的now()函數來取得當前的時間戳記,你可以這樣做:

import knex from 'knex';

const currentTime = knex.fn.now();

knex('table_name')
  .update({ updated_at: currentTime })
  .where('id', '=', 1)
  .then((result) => {
    console.log(result);
  });

在這個例子中,我們使用了.fn()方法來呼叫SQL的now()函數,並將其存儲在currentTime變數中。接著,我們將這個變數用於Knex的update查詢中,以更新updated_at欄位。

.fn()方法提供了一種更直觀的方式來使用SQL內建函數,而不需要直接使用原始的SQL語句。

總結來說,除了使用.raw()方法之外,你還可以使用.fn()方法來整合SQL內建函數到你的Knex查詢中,這樣可以使你的代碼更具可讀性和維護性。

https://ithelp.ithome.com.tw/upload/images/20231103/20135750eLaE2jNea7.png

當使用Knex進行SQL查詢時,.fn()方法用於整合SQL內建函數到你的查詢中。這個方法提供了一種較為清晰和可讀性較高的方式來使用這些函數,而不需要撰寫原始的SQL語句。以下是關於.fn()的詳細教學,以及與.raw()的比較:

使用 .fn() 方法:

  1. 引入 Knex

    首先,確保你已經引入Knex,並且建立了Knex的實例。

    const knex = require('knex')({
      client: 'mysql', // 選擇你使用的資料庫
      connection: {
        host: 'localhost',
        user: 'your_username',
        password: 'your_password',
        database: 'your_database',
      },
    });
    
    
  2. 使用 .fn() 方法

    使用.fn()方法可以呼叫SQL內建函數,並將其整合到你的Knex查詢中。

    const currentTimestamp = knex.fn.now();
    
    knex('your_table')
      .select(currentTimestamp)
      .then((result) => {
        console.log(result);
      });
    

    在這個例子中,我們使用.fn.now()來呼叫SQL的now()函數,然後將其整合到select查詢中,以取得當前的時間戳記。

不同於 .raw() 方法:

  1. .fn()更安全

    使用.fn()方法可以幫助防止SQL注入攻擊,因為Knex會處理函數的參數,確保它們不會被視為SQL代碼的一部分。這使你的應用程序更加安全。

  2. 更可讀性

    .fn()提供了一種更具可讀性的方式來使用SQL內建函數,而不需要在代碼中嵌入原始的SQL語句。這使代碼更易於理解和維護。

  3. 跨資料庫相容性

    使用.fn()方法可以提高代碼的跨資料庫相容性,因為不同的資料庫可能具有不同的函數和語法。.fn()會自動根據所使用的資料庫引擎生成正確的SQL語句。

總之,.fn()方法提供了一個更安全、可讀性較高、跨資料庫相容的方式來整合SQL內建函數到你的Knex查詢中,相較於.raw()方法更受推薦。當你需要使用SQL內建函數時,.fn()通常是更好的選擇。

migration(遷移)

接下來探討的是migration
在上一篇中,ChatGPT有提到knex的其中一個功能是可以做migration

這個是什麼呢?作用和目的是什麼,我們一起來問ChatGPT
https://ithelp.ithome.com.tw/upload/images/20231103/20135750T87dh63cCm.png

Migration 是一種軟體開發實踐,它用於管理和跟蹤資料庫模式的變化。在開發過程中,隨著需求的變化或新功能的添加,資料庫的結構可能會需要進行修改,這時就需要使用 migration 來管理這些變化。

以下是 migration 的詳細介紹:

作用:

  1. 保持資料庫的版本控制

    Migration 允許你在專案的不同階段保持資料庫的版本控制。每個 migration 檔案代表了一個特定版本的資料庫結構,這樣你可以隨時回到過去的版本或前進到新的版本。

  2. 跟蹤資料庫模式的變化

    通過使用 migration,你可以跟蹤資料庫模式的變化,包括新增表格、刪除表格、修改欄位等。這使得團隊成員可以了解資料庫結構的演變。

  3. 在多個環境中保持一致性

    在開發、測試和生產環境中,資料庫結構可能會有所不同。透過使用 migration,你可以確保在不同環境中的資料庫結構保持一致。

Migration 流程:

  1. 創建 Migration 檔案

    每次你需要修改資料庫結構時,你會創建一個新的 migration 檔案,並在其中定義這些變化。

  2. 執行 Migration

    使用 migration 工具(如Knex CLI)執行這些 migration 檔案,將變化應用到資料庫中。

  3. 記錄 Migration 狀態

    當 migration 成功應用後,會在資料庫中記錄相應的狀態,標記哪些 migration 已經被應用。

  4. 回滾 Migration

    如果需要回到之前的版本,可以使用 migration 工具回滾到先前的狀態,並還原資料庫結構。

優點:

  1. 可追蹤和可管理

    透過 migration,你可以清晰地追蹤資料庫結構的變化,使其可管理。

  2. 版本控制

    可以使用版本控制系統(如Git)來管理 migration 檔案,保證在不同版本之間的一致性。

  3. 多環境適用

    可以在不同環境中(開發、測試、生產)保持資料庫結構的一致性。

缺點:

  1. 學習成本

    需要一些時間來學習和習慣使用 migration 工具,特別是對於初學者來說可能會有一定的學習曲線。

  2. 可能需要手動處理特殊情況

    在某些情況下,可能需要手動處理一些特殊的資料庫變化,而不是依賴 migration 工具自動處理。

總的來說,使用 migration 是一個良好的實踐,可以幫助你管理和跟蹤資料庫結構的變化,保持資料庫的一致性和可追蹤性。

這麼說起來,有時在工作時,會有測試環境的資料庫、正式資料庫等
有些時候在測試資料庫追加或更改了一些欄位

但上到正式之後因為沒有進行同步,又沒有好好的開出release分支測試
導致上線後服務掛掉,要兩邊對照,到底兩邊資料庫的差異是什麼
更改對應資料欄位之後服務才正常
如果資料庫的schema也能得到版本控制,對於管理上會更穩定

了解用途與優點後,來看看這個機制實際上是怎麼運作的
https://ithelp.ithome.com.tw/upload/images/20231103/20135750hfnqw7jYmu.png

Migration的運作原理涉及到了幾個關鍵的概念和步驟,讓我來為你進行更詳細的解析與說明:

1. Migration Table:

在Migration的運作中,通常會建立一個特殊的資料表(通常稱為migrations或類似的名稱)來記錄已經被應用的migration的狀態。

這個表通常包含以下欄位:

  • id: 用來唯一識別每個migration。
  • name: migration的名稱或標識。
  • batch: migration所屬的批次,通常用來記錄這個migration是在哪一個階段被應用的。

2. Migration Files:

Migration通常以檔案的形式存在,每個migration都是一個獨立的檔案,這些檔案被組織在一個特定的目錄中。

這些檔案通常包含以下內容:

  • up: 定義了這個migration要執行的升級操作,也就是要將資料庫結構升級到這個版本。
  • down: 定義了這個migration的回滾操作,也就是要將資料庫結構還原到這個版本之前的狀態。

3. Migration操作流程:

  1. 執行新的Migration

    當你執行一個新的Migration時,migration工具會讀取這個檔案,並將其中的升級操作(up)應用到資料庫中,同時記錄下這個migration已經被應用。

  2. 記錄Migration狀態

    在資料庫的migrations表中記錄下這個migration的狀態,包括它的名稱、批次等信息,這樣可以在未來追蹤和管理這個migration。

  3. 回滾Migration

    如果需要回滾到先前的版本,migration工具會讀取相應的migration檔案,並執行其中的回滾操作(down),同時在migrations表中更新相關的狀態。

4. 執行順序和版本控制:

通常,migration工具會根據migrations表中的狀態,確定要執行哪些migration以及它們的執行順序。這樣可以保證資料庫結構的演進是有序的,並且可以隨時回滾到先前的版本。

5. 特殊情況的處理:

在某些情況下,可能需要手動處理一些特殊的資料庫變化,這時可以在migration的回滾操作(down)中進行相應的處理。

總的來說,Migration通過管理和追蹤資料庫模式的變化,提供了一個有效的方式來保持資料庫的版本控制,並確保在不同版本之間的一致性。它涉及到創建migration檔案、執行升級和回滾操作,以及在migrations表中記錄相應的狀態。這使得開發團隊可以輕鬆地追蹤和管理資料庫結構的變化,並保證在不同環境中的一致性。

接下來直接來上教學吧!

設置 & migration基礎語法

https://ithelp.ithome.com.tw/upload/images/20231103/20135750vl7J60Y0x1.png

當你在NestJS專案中已經安裝好Knex和mysql2時,接下來我們會進行以下步驟:

  1. 創建 knexfile.ts
  2. 創建 Migration

步驟 1: 創建 knexfile.ts

首先,在你的NestJS專案根目錄下,創建一個名為 knexfile.ts 的檔案。這個檔案將包含Knex的配置信息。

// knexfile.ts

import { Knex } from 'knex';

const config: Knex.Config = {
  client: 'mysql2', // 選擇你使用的資料庫
  connection: {
    host: 'localhost',
    user: 'your_username',
    password: 'your_password',
    database: 'your_database',
  },
  migrations: {
    directory: __dirname + '/src/migrations', // 告訴Knex migration檔案的存放位置
    tableName: 'knex_migrations', // 設定migration記錄的資料表名稱
  },
};

export default config;

在這個例子中,我們使用了MySQL2作為資料庫客戶端,並提供了相應的連線資訊。同時,我們也配置了migration相關的設定,告訴Knex migration檔案的存放位置和記錄migration狀態的資料表名稱。

步驟 2: 創建 Migration

接下來,我們將創建一個migration來示範如何進行資料庫模式的變化。

在終端中執行以下指令以創建一個migration:

npx knex migrate:make create_users_table

這會在 src/migrations 目錄下創建一個新的migration檔案,檔案名稱會包含timestamp和migration名稱(create_users_table)。

打開新創建的migration檔案,並在 up 方法中定義創建users表格的操作,並在 down 方法中定義回滾這個操作的邏輯。

// 例: 20231117123456_create_users_table.ts

import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
  return knex.schema.createTable('users', (table) => {
    table.increments('id').primary();
    table.string('name');
    table.string('email').unique();
    table.timestamps(true, true);
  });
}

export async function down(knex: Knex): Promise<void> {
  return knex.schema.dropTable('users');
}

最後,執行以下指令來應用這個migration:

npx knex migrate:latest

這會將users表格創建到你的資料庫中。

現在,你已經完成了NestJS專案搭配Knex和MySQL進行migration的設定和操作。當你需要進行資料庫結構變更時,只需創建一個新的migration並執行即可。

照著做之後,跑出了Batch 1 run: 1 migration 的訊息
基本上就是初次的設定成功了
https://ithelp.ithome.com.tw/upload/images/20231103/20135750bf0YeYkXyJ.png

然後來看看資料庫這邊,除了之前練習時就有的表格之外,多出了他所指定的users表格
https://ithelp.ithome.com.tw/upload/images/20231103/20135750tSWteQMYvZ.png

以及另外兩張管理migration用的表:knex_migrationsknex_migrations_lock
也是一開始在講解詳細流程的時候,ChatGPT說的Migration Table
https://ithelp.ithome.com.tw/upload/images/20231103/20135750zYVoOITUDL.png

https://ithelp.ithome.com.tw/upload/images/20231103/20135750N33hWgW85h.png

請ChatGPT再細細講一下這兩個表格的作用
https://ithelp.ithome.com.tw/upload/images/20231103/20135750j4MyD0aQFV.png

當你執行 npx knex migrate:latest 時,Knex 會自動創建兩個特殊的資料表:knex_migrationsknex_migrations_lock,這兩個表格是用來管理 Migration 的狀態的。

1. knex_migrations 資料表:

knex_migrations 是一個用來記錄已經被應用的 migrations 的表格。這個表格包含了以下欄位:

  • id: 一個唯一的識別符,通常是一個自增的數字。
  • name: Migration 的名稱或標識,通常是對應 migration 檔案的名稱。
  • batch: Migration 所屬的批次,用來標識這個 migration 是在哪一個階段被應用的。

knex_migrations 會隨著每次執行 migration 指令而被更新,記錄了已經被應用的 migrations。

2. knex_migrations_lock 資料表:

knex_migrations_lock 是用來實現 migration 鎖定機制的表格。當你執行 npx knex migrate:latest 或其他 migration 相關的指令時,Knex 會先對這個表格進行操作,以確保同一時間只有一個 migration 在運行。

這個表格只包含一個欄位:

  • is_locked: 一個布林值,用來表示 migration 是否被鎖定。當一個 migration 開始執行時,會將此欄位設為 true,執行完畢後再設為 false

透過這個表格,Knex 可以確保同一時間只有一個 migration 在運行,避免了可能的併發問題。

總的來說,knex_migrationsknex_migrations_lock 這兩個表格是 Knex 用來管理 migration 狀態和實現鎖定機制的關鍵元素。它們確保了 migration 的執行是有序的,並避免了可能的併發問題。

搭個流程圖!
https://ithelp.ithome.com.tw/upload/images/20231103/20135750VNZt2ciqR9.png

我可以為你提供文字描述,但是目前無法直接以Mermaid語法繪製流程圖。請參考以下描述:

  1. 執行 npx knex migrate:latest 指令。
  2. Knex 首先檢查 knex_migrations_lock 資料表,看是否有鎖定。
  3. 如果 knex_migrations_lock 中的 is_locked 欄位為 true,表示已經有其他 migration 正在執行,Knex 會等待直到該 migration 完成。
  4. 如果 knex_migrations_lock 中的 is_locked 欄位為 false,Knex 會將其設為 true,表示這個 migration 已經開始執行。
  5. Knex 接著開始執行 migration,依次應用每一個尚未被應用的 migration。
  6. 對於每一個 migration,Knex 會先在 knex_migrations 資料表中記錄相關的狀態,包括 migration 的名稱和批次。
  7. 接著,Knex 會執行 migration 中的 up 方法,將資料庫結構升級到對應版本。
  8. 完成 migration 後,Knex 會在 knex_migrations 中更新相關的狀態,標識這個 migration 已經被應用。
  9. 最後,Knex 會將 knex_migrations_lock 中的 is_locked 欄位設為 false,表示這個 migration 完成執行。

這個流程保證了在同一時間只有一個 migration 在執行,並且 migration 的執行是有序的。如果在執行期間有其他 migration 同時進行,Knex 會等待直到前一個 migration 完成後再執行下一個。這樣可以避免可能的併發問題。

鍥而不捨,再來一次
https://ithelp.ithome.com.tw/upload/images/20231103/20135750QKamTzaH8w.png

抱歉,我之前的回答有誤,事實上我可以使用Mermaid語法繪製流程圖,以下是 npx knex migrate:latest 執行時 knex_migrationsknex_migrations_lock 的運作原理流程圖:

graph TD
    subgraph User
    A[執行 'npx knex migrate:latest']
    end

    subgraph Knex_Migrations_Lock
    B[檢查 'knex_migrations_lock' 中的 'is_locked']
    C[設定 'is_locked' 為 true]
    D[等待]
    E[設定 'is_locked' 為 false]
    end

    subgraph Knex_Migrations
    F[記錄 migration 的狀態]
    G[執行 migration 的 'up' 方法]
    H[更新 migration 的狀態]
    end

    A --> B
    B --|is_locked=false|--> C
    B --|is_locked=true|--> D
    C --> F
    F --> G
    G --> H
    H --> E
    D --> E

上述流程圖說明了執行 npx knex migrate:latest 時,Knex 如何操作 knex_migrationsknex_migrations_lock 這兩個表格。希望這能幫助你更好地理解運作原理。

https://ithelp.ithome.com.tw/upload/images/20231103/20135750hAAXbRtwlW.png

追加 migrations

今天如果我還要針對表格去做變動,例如上面的users表格要追加phone欄位的話?
來看看使用migrations的機制要怎麼做
https://ithelp.ithome.com.tw/upload/images/20231103/20135750I5vilLgslX.png

要追加一個 phone 欄位到 users 表格中,你可以創建一個新的 migration 來實現這個變化。以下是具體的步驟:

步驟 1: 創建 Migration

在終端中執行以下指令以創建一個新的 migration:

npx knex migrate:make add_phone_to_users_table

這會在 src/migrations 目錄下創建一個新的 migration 檔案,檔案名稱會包含 timestamp 和 migration 名稱(add_phone_to_users_table)。

步驟 2: 編輯 Migration 檔案

打開新創建的 migration 檔案,並在 up 方法中定義添加 phone 欄位的操作,並在 down 方法中定義回滾這個操作的邏輯。

// 例: 20231118123456_add_phone_to_users_table.ts

import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
  return knex.schema.table('users', (table) => {
    table.string('phone');
  });
}

export async function down(knex: Knex): Promise<void> {
  return knex.schema.table('users', (table) => {
    table.dropColumn('phone');
  });
}

步驟 3: 執行 Migration

最後,執行以下指令來應用這個新的 migration:

npx knex migrate:latest

這會將 phone 欄位添加到你的 users 表格中。

現在,你的 users 表格應該包含了一個新的 phone 欄位。請注意,這個 migration 也會被記錄在 knex_migrations 中,以便在未來進行回滾或追蹤變化。

執行完之後,knex_migrations 的表格追加了一行,紀錄了已經有執行過了
https://ithelp.ithome.com.tw/upload/images/20231103/20135750alilhGjy7o.png

如果要再度執行 npx knex migrate:latest,就會顯示已經到最新了!
https://ithelp.ithome.com.tw/upload/images/20231103/20135750rSpTn68c5V.png

遷移資料庫

接下來我們要試看看,用現有的migrations檔案,來更新到新的DB中
https://ithelp.ithome.com.tw/upload/images/20231103/20135750OuQNN5040Q.png

如果你想在一個新的資料庫(例如名為 newDB)上執行相同的 migrations,你需要做以下幾個步驟:

步驟 1: 創建新的資料庫

首先,確保你已經在你的資料庫伺服器上創建了一個名為 newDB 的新資料庫。

步驟 2: 修改 knexfile.ts

在你的 NestJS 專案中,打開 knexfile.ts 檔案,修改 connection 部分以指向新的資料庫 newDB

// knexfile.ts

import { Knex } from 'knex';

const config: Knex.Config = {
  client: 'mysql2',
  connection: {
    host: 'localhost',
    user: 'your_username',
    password: 'your_password',
    database: 'newDB', // 修改為新的資料庫名稱
  },
  migrations: {
    directory: __dirname + '/src/migrations',
    tableName: 'knex_migrations',
  },
};

export default config;

步驟 3: 執行 Migration

現在,你可以執行以下指令來應用 migrations 到新的資料庫 newDB 上:

npx knex migrate:latest

這將會在 newDB 上創建相應的表格和結構。

請確保你在運行指令之前已經在資料庫伺服器上創建了 newDB 資料庫。這樣你就可以在新的資料庫上執行相同的 migrations 了。

OK,所以首先加一個新的DB (或是本來就有其他目的資料庫)
https://ithelp.ithome.com.tw/upload/images/20231103/201357508q2QEvm61Y.png

然後更改一下 knexfile.ts 的連線database名稱
https://ithelp.ithome.com.tw/upload/images/20231103/20135750NxhOfM0Kwi.png

執行指令之後
因為會依序執行目前的兩個migrations的「up」的內容

就可以看到newDB上有一樣的表格和記錄了!
https://ithelp.ithome.com.tw/upload/images/20231103/20135750fpPWDpkZfG.png

其他DB工具介紹

上一篇也有說了,knex不一定是處理資料庫的首選
他的缺點我個人是覺得學習資源比較少(雖然可以用ChatGPT補足)

請ChatGPT來介紹其他可以做migration 的工具與優缺點比較!
https://ithelp.ithome.com.tw/upload/images/20231103/20135750hZu586g9Ja.png

除了 Knex 之外,還有一些其他 Node.js 工具套件可以處理 Migration。以下是其中幾個常用的:

  1. Sequelize:
    • 特色:
      • Sequelize 是一個支援多種資料庫的 ORM (Object-Relational Mapping) 庫,可以簡化資料庫操作。
      • 提供了內建的 Migration 功能,允許你通過定義 Migration 文件來進行資料庫結構的變化。
      • 支援多種資料庫類型,包括 MySQL、PostgreSQL、SQLite 等。
    • 優點:
      • 集成了 ORM 功能,使得資料庫操作更加便捷和直觀。
      • 支援多種資料庫,可以輕鬆切換不同的資料庫類型。
    • 缺點:
      • 學習曲線較陡,尤其對於初學者來說可能需要一些時間適應。
      • 在一些特定情況下,可能需要手動處理一些複雜的資料庫操作。
  2. TypeORM:
    • 特色:
      • TypeORM 是一個 TypeScript 環境下的 ORM 庫,提供了類似於 Sequelize 的功能,但更注重於 TypeScript 的特性和類型安全。
      • 支援多種資料庫,包括 MySQL、PostgreSQL、SQLite 等。
      • 具有內建的 Migration 功能,可以通過指令來創建和執行 Migration。
    • 優點:
      • 提供了 TypeScript 支援,具有嚴格的類型檢查,使得開發過程更加安全和穩定。
      • 支援多種資料庫,並提供了豐富的功能和選項。
    • 缺點:
      • 學習曲線較陡,尤其對於初學者來說可能需要一些時間適應。
  3. Bookshelf.js:
    • 特色:
      • Bookshelf.js 是一個資料庫查詢和ORM庫,建立在Knex之上,對於簡化資料庫操作非常有用。
      • 提供了簡單的Model定義和關聯,方便操作資料庫。
    • 優點:
      • 簡單易用,對於小型項目或需要輕量級 ORM 功能的場景非常適用。
      • 具有良好的文件和社群支援。
    • 缺點:
      • 功能相對於 Sequelize 或 TypeORM 來說較為簡單,可能不適用於複雜的資料庫操作。
  4. Objection.js:
    • 特色:
      • Objection.js 是基於 Knex 的資料庫查詢和ORM庫,提供了類似於 Bookshelf.js 的功能,但具有更多的擴展性和功能。
    • 優點:
      • 可以方便地定義模型和關聯,支援更複雜的查詢和操作。
      • 有良好的文件和活躍的社群支援。
    • 缺點:
      • 學習曲線可能較陡,尤其對於初學者來說可能需要一些時間適應。

選擇使用哪個工具套件取決於你的專案需求、團隊熟悉度以及個人偏好。每個工具套件都有其獨特的特色和優缺點,請根據自己的需求做出適當的選擇。

統整重點

https://ithelp.ithome.com.tw/upload/images/20231103/20135750gWOHvOYbE2.png

在這次的對話中,我們討論了關於資料庫 Migration 的相關主題。以下是一些重點摘要:

  1. Knex.js 是一個強大的 Node.js 資料庫查詢和 Migration 工具,它提供了一種簡單且直觀的方式來管理資料庫結構的變化。
  2. Migration 是用來管理和追蹤資料庫結構變化的過程,它包括了創建 Migration 檔案、執行升級和回滾操作,以及在 knex_migrations 表中記錄相應的狀態。
  3. Transactions 可以保證一系列的資料庫操作要麼全部成功要麼全部失敗,這對於確保資料庫的一致性非常重要,特別是在處理敏感數據或者涉及多個操作的情況下。
  4. 模組化查詢可以幫助你維護複雜的資料庫操作,它允許你將多個查詢劃分成小的模組,提高了代碼的可讀性和可維護性。
  5. 內建函數(例如 now())可以在 Knex 中使用 .raw().fn() 進行操作,這允許你在 SQL 查詢中使用資料庫特定的內建函數。

此外,我們還探討了一些其他的 Node.js 工具套件,如 Sequelize、TypeORM、Bookshelf.js 和 Objection.js,它們也可以用於處理 Migration。

總的來說,選擇適合自己專案的 Migration 工具是根據專案需求、團隊熟悉度和個人偏好來做出的重要決策。希望這些訊息對你在開發前端工程師的過程中有所幫助!如果你還有其他問題,請隨時向我詢問。

結語

本次對話記錄在這邊:
https://chat.openai.com/share/9ae3bb71-c5c5-4fca-aaad-ed62080a0946

嚴格上來說,Migration 完全可以切出去變成獨立的一篇
本來是想介紹knex的進階功能,主要要講transaction

我原先對於Migration 也不是很熟,只有大概知道有這個功能,但沒有實際操作過
不過這樣走過一次,也算是有知道要怎麼操作了!
雖然語法還不會!但我們有ChatGPT沒問題的(?)

那到這邊為止,和NestJS相關的主題就到這邊告一段落

這個系列文從Day1到Day50都是日更,雖然開賽前有事先準備五篇草稿
不過從開賽以來,仍然維持每天 0.5~1.5篇左右的進度,積存草稿在1~5篇之間遊走
到今天為止就沒有任何儲存文章了XD

總之!「點亮前後端技能樹」的部分這邊先告一個小段落

接下來會是「ChatGPT幫我完成工作!」的小系列
主要是講述我自己在ChatGPT推出了之後,
常常請ChatGPT如何幫我完成工作,或是解決工作上的問題

謝謝各位的訂閱與閱讀,和我一起點亮技能樹
我是目標讓技能樹亮晶晶的前端工程師一宵三筵,我們也許明天見!


上一篇
【Day49】ChatGPT請教教我:NestJS!(八)- SQL & Knex!SQL注入攻擊!?
下一篇
【Day51】ChatGPT幫我完成工作:直接詠唱來重構翻新!PHP 轉NestJS + Knex!
系列文
用ChatGPT詠唱來完成工作與點亮前後端技能樹56
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言