更新於 2024/10/20閱讀時間約 24 分鐘

第三方金流串接 – 產品功能Model製作

    ※ 認識MVC架構:

    raw-image

    ※ 認識lodash:

    Lodash 是一個很實用的 JavaScript 函式庫。它提供了很多預設的實用函數,可以更容易地處理數據、操作物件和字串,並控制函數的執行。目的是為了簡化重複性的代碼,減少開發時間和錯誤。

    Lodash 中常用的功能:

    1. 陣列處理:_.map, _.filter, _.reduce 等函數,讓你可以方便地操作陣列。
    2. 物件處理:_.assign, _.cloneDeep, _.merge 等函數,讓物件的操作變得簡單。
    3. 數據查詢:_.find, _.findIndex, _.includes 等函數,幫助你快速查找數據。
    4. 函數處理:_.debounce, _.throttle, _.once 等函數,控制函數的調用次數。
    5. 字串處理:_.camelCase, _.kebabCase, _.startCase 等函數,方便格式化字串。

    ※ 認識@types/lodash:

    這個是 TypeScript 的型別定義包,用於 Lodash 函式庫。當你在用 TypeScript 開發時,它提供 Lodash 函數的型別定義,這樣編譯器在使用 Lodash 時得到自動完成功能和型別檢查。如果只是用 JavaScript,那就不需要下載這個型別定義包。

    ※ 建立Model :

    在 MVC (Model-View-Controller) 架構中,Model 是負責處理資料邏輯和業務邏輯的部分。Model 的主要功能有:

    • 資料管理:Model 負責與資料庫交互,執行 CRUD(建立、讀取、更新、刪除)操作。
    • 業務邏輯:Model 處理應用程式的業務邏輯,確保數據的正確性和一致性。
    • 資料封裝:Model 將資料封裝成物件,提供簡單的介面給 Controller 使用。

    ※ 在 Model 資料夾中建立 base.ts 檔案,處理通用資料庫操作:


    ※ 定義資料庫操作 :

    export interface IBase<T> {
    findAll(trx?: Knex.Transaction): Promise<T[] | null>;
    findOne(id: any, trx?: Knex.Transaction): Promise<T | null>;
    create(data: Omit<T, 'id'>, trx?: Knex.Transaction): Promise<T | null>;
    update(id: any, data: Partial<Omit<T, 'id'>>, trx?: Knex.Transaction): Promise<T | null>;
    delete(id: any, trx?: Knex.Transaction): Promise<void>;
    }

    程式碼解說:

    • interface 是用來定義物件結構的一種方式。它描述了物件應該有哪些屬性及其類型。
    • IBase 是一個介面,用來定義一組針對資料庫操作的常見方法。
    • <T>只是個占位符,它代表的是未來會被傳入的具體類型。等到其他類別或介面繼承 IBase 時,才會傳入真正的類型。
    • trx?表示這個參數是可選的,不傳遞也沒關係。它是 Knex.js 用於資料庫交易(transaction)的物件。
    • Omit:目的是排除某個屬性。在創建資料時,由系統自動生成或管理的屬性,有ID、時間戳(created_at 和 updated_at)、唯一標識碼(UUID)、系統內部狀態。
    • findAll(trx?: Knex.Transaction): Promise<T[] | null>; 查找所有記錄,返回一個包含所有記錄的 Promise,類型為陣列(T[]),或是空值(null)。
    • findOne(id: any, trx?: Knex.Transaction): Promise<T | null>; 根據 ID 查找單一記錄,返回一個包含該記錄的 Promise,類型為 T 或是空值(null)。
    • create(data: Omit<T, 'id'>, trx?: Knex.Transaction): Promise<T | null>; 創建一個新的記錄,傳入的資料不包含 ID(Omit<T, 'id'>),返回一個包含創建結果的 Promise,類型為 T 或是空值(null)。
    • update(id: any, data: Partial<Omit<T, 'id'>>, trx?: Knex.Transaction): Promise<T | null>: 說的是,更新指定 ID 的記錄,傳入需要修改的屬性,返回一個包含更新結果的 Promise,類型為 T 或是空值(null)。
    • delete(id: any, trx?: Knex.Transaction): Promise<void>; 根據 ID 刪除記錄,返回一個 Promise,完成後不返回數據。
    • T 表示單一的一個資料元素,而 T[] 則表示一個包含多個資料元素的陣列。也就是說,查詢所有資料用 T[] 表示資料陣列,查詢單一資料或創建新資料用 T 表示單個資料。
    • void 是 TypeScript 和 JavaScript 中用來表示「沒有返回值」的函數。

    ※ 建立資料庫模型基礎框架:

    export abstract class Base<T> implements IBase<T> {
    protected knexSql: Knex;
    protected tableName: string = '';
    protected schema = {};
    constructor({ knexSql, tableName }: { knexSql: Knex, tableName?: string }) {
    this.knexSql = knexSql;
    if (tableName) this.tableName = tableName;
    }

    程式碼解說:

    1. export abstract class Base<T> implements IBase<T>
      • 定義了一個泛型抽象類別 Base這個框架,來實現 IBase<T> 這個介面。這表示 Base 類別必須包含 IBase 介面定義的方法,但因為它是抽象類別,所以不能直接實例化。
    2. protected knexSql: Knex;
      • 定義了一個受保護的屬性 knexSql,類型為 Knex。這表示這個屬性只能在 Base 類別及其子類中使用。目的是增強程式碼的封裝性和安全性
    3. protected tableName: string = '';
      • 定義了一個受保護的屬性 tableName,類型為 string,並預設值為空字串。
    4. protected schema = {};
      • 定義了一個受保護的屬性 schema,預設為空物件。這可能用來存儲資料庫表格的結構定義。
    5. constructor({ knexSql, tableName }: { knexSql: Knex, tableName?: string })
      • 定義了一個建構函數,接受一個包含 knexSql 和可選 tableName 的物件作為參數。
      • this.knexSql = knexSql;:將傳入的 knexSql 賦值給類別屬性。
      • if (tableName) this.tableName = tableName;:如果傳入了 tableName,就將其賦值給類別屬性。

    ※ 查詢所有記錄:

    public findAll = async (trx?: Knex.Transaction) => {
    //select col1, col2, ...from tableName
    let sqlBuilder = this.knexSql(this.tableName).select(this.schema);

    if (trx) sqlBuilder = sqlBuilder.transacting(trx);

    const result = await sqlBuilder;

    if (isEmpty(result)) return null;

    return result.map(this.DBData2DataObject) as T[];
    }

    程式碼解說:

    1. 方法定義
    public findAll = async (trx?: Knex.Transaction) => {}
    • public:表示這個方法是公開的,可以被外部訪問。
    • findAll:方法名稱。
    • async:表示這個方法是非同步的,會返回一個 Promise。
    • trx?: Knex.Transaction:可選的參數 trx,類型為 Knex 的交易物件。
    1. 建立 SQL 查詢
    let sqlBuilder = this.knexSql(this.tableName).select(this.schema);
    • 使用 Knex.js 建立一個 SQL 查詢建構物件(sqlBuilder),從 this.tableName 表中選擇 this.schema 中定義的欄位。
    1. 交易處理
    if (trx) sqlBuilder = sqlBuilder.transacting(trx);

    如果有提供 trx 交易物件,則將查詢納入該交易,以確保資料的一致性和原子性。

    1. 執行查詢
    const result = await sqlBuilder;
    • 執行查詢並等待結果。
    1. 結果處理
    if (isEmpty(result)) return null;
    return result.map(this.DBData2DataObject) as T[];
    • 如果結果為空,返回 null。
    • 如果有結果,會將結果轉換成資料物件,並以 T[] 型別返回。

    ※ 查詢單筆記錄:

    public findOne = async (id: any, trx?: Knex.Transaction) => {
    let sqlBuilder = this.knexSql(this.tableName).select(this.schema).where(id);

    if (trx) sqlBuilder = sqlBuilder.transacting(trx);

    const result = await sqlBuilder;

    if (isEmpty(result)) return null;

    return this.DBData2DataObject(result[0]) as T;
    }

    ※ 建立單筆記錄:

    public create = async (data: Omit<T, 'id'>, trx?: Knex.Transaction) => {
    let sqlBuilder = this.knexSql(this.tableName).insert(this.DataObject2DBData(data));

    if (trx) sqlBuilder = sqlBuilder.transacting(trx);

    const result = await sqlBuilder;

    if (isEmpty(result)) return null;
    const id = result[0];
    return await this.findOne(id, trx);

    }

    程式碼解說:

    1. 方法定義
    public create = async (data: Omit<T, 'id'>, trx?: Knex.Transaction) => {
    • data: Omit<T, 'id'>:傳入的資料不包含 id 屬性,類型為 T(除了 id)。
    1. 建立 SQL 插入查詢
    let sqlBuilder = this.knexSql(this.tableName).insert(this.DataObject2DBData(data));
    • 使用 Knex.js 建立一個插入查詢,將資料插入到 this.tableName 表中,資料經過 this.DataObject2DBData(data) 處理。
    1. 結果處理
    if (isEmpty(result)) return null;
    const id = result[0];
    return await this.findOne(id, trx);
      • 如果有結果,提取插入記錄的 id,並使用 findOne 方法根據 id 查詢並返回新創建的記錄。

      ※ 更新單筆記錄:

      public update = async (id: any, data: Partial<Omit<T, 'id'>>, trx?: Knex.Transaction) => {
      let sqlBuilder = this.knexSql(this.tableName).update(this.DataObject2DBData(data).where({ id }));

      if (trx) sqlBuilder = sqlBuilder.transacting(trx);

      await sqlBuilder;

      return await this.findOne(id, trx);}

      ※ 刪除單筆記錄:

      public delete = async (id: any, trx?: Knex.Transaction) => {
      let sqlBuilder = this.knexSql(this.tableName).where({ id }).del();

      if (trx) sqlBuilder = sqlBuilder.transacting(trx);

      await sqlBuilder;

      return

      }
      程式碼解說:
      return;
      • 方法完成後不返回任何數據,因為這個方法的主要目的是刪除指定記錄。

      ※ 將資料庫中的資料轉換為應用程式中的資料格式:

      private DBData2DataObject = (data: any) => {
      const transform = mapValues(data, (value, key) => {
      if (['updatedAt', 'createdAt'].includes(key)) return new Date(value);
      if (isJson(value)) return JSON.parse(value);
      return value;
      });
      return mapKeys(transform, (_value, key) => camelCase(key));
      }

    程式碼解說:

    1. mapValues:對資料進行處理,針對每個鍵值對進行操作。
      • 日期轉換:如果鍵是 updatedAt 或 createdAt,將其值轉換為 Date 型別。
      • JSON 解析:如果值是 JSON 字串,解析成對象。
      • 其他值:保持不變。
    2. mapKeys:將鍵轉換為駝峰式命名(camelCase),更符合 JavaScript 的命名慣例。

    ※ 將應用程式中的資料轉換為資料庫中的資料格式:

    private DataObject2DBData = (data: any) => {
    const transform = mapValues(data, (value, key) => {
    if (['updatedAt', 'createdAt'].includes(key)) return new Date(value);
    if (isJson(value)) return JSON.parse(value);
    return value;
    });
    return mapKeys(transform, (_value, key) => snakeCase(key));
    }

    程式碼解說:

    1. mapKeys:將鍵轉換為蛇形命名(snake_case),更符合資料庫的命名慣例。

    總結:

    • DBData2DataObject:將資料庫資料轉換為應用程式資料,使用駝峰式命名。
    • DataObject2DBData:將應用程式資料轉換為資料庫資料,使用蛇形命名。

    ※ 判斷輸入的字串是否為有效的 JSON 格式:

    export const isJson = (value: string) => {
    try {
    return Boolean(JSON.parse(value));
    } catch (e) {
    return false;
    }
    }
    1. 函數定義
    export const isJson = (value: string) => {}
    • export:表示這個函數可以被其他模組引用。
    • const isJson:定義了一個常數函數,名稱為 isJson。
    • (value: string):函數接受一個字串類型的參數 value。
    1. try 區塊:
    try {
    return Boolean(JSON.parse(value));
    }
    • JSON.parse(value):嘗試將字串解析為 JSON。
    • Boolean():將解析結果轉換為布林值。如果解析成功,返回 true。
    1. catch 區塊
    catch (e) {
    return false;
    }
    • 如果解析失敗,捕捉錯誤並返回 false。

    ※ Model資料夾內 建立產品檔案 – product.ts

    ※ 定義Product 的 介面:

    export interface Product {
    //符合 TypeScript 的語法和類型系統
    id: number;
    name: string;
    amount: number;
    description: string;
    pre_order: number;
    price: number;
    }

    程式碼解說:

    • id: number;:產品的唯一標識符,類型為數字。
    • name: string;:產品名稱,類型為字串。
    • amount: number;:產品數量,類型為數字。
    • description: string;:產品描述,類型為字串。
    • pre_order: number;:預購數量,類型為數字。
    • price: number;:產品價格,類型為數字。

    ※ 定義IProductModel 的 介面:

    export interface IProductModel extends IBase<Product> {}

    程式碼解說:

    1. export interface IProductModel extends IBase<Product> {}:
      • export:表示這個介面可以被其他模組引用。
      • interface IProductModel:定義了一個介面 IProductModel。
      • extends IBase<Product>:表示 IProductModel 繼承自 IBase<Product>。
    2. IBase<Product>:這表示 IProductModel 繼承了 IBase 介面的所有方法和屬性,而這些方法和屬性的泛型類型為 Product。這意味著 IProductModel 會擁有 IBase 定義的所有 CRUD 操作(如 findAll, findOne, create, update, delete),但這些操作針對的是 Product 類型的資料。

    簡單來說,IProductModel 介面是 IBase 介面的具體化,專門用於操作 Product 類型的資料。這樣定義能讓代碼更具模組化和可重用性。

    ※ 實作class:

    1. 類別定義
    export class ProductModel extends Base<Product> implements IProductModel {}

    程式碼解說:

    • export class ProductModel:定義並導出一個名為 ProductModel 的類別。
    • extends Base<Product>:表示這個類別繼承自 Base<Product> 類別。
    • implements IProductModel:表示這個類別實現了 IProductModel 介面。
    1. tableName 屬性:
    tableName = 'products';

    程式碼解說:

    • 定義資料表的名稱為 products。
    1. schema 屬性
    schema = {
    id: 'id',
    name: 'name',
    amount: 'amount',
    description: 'description',
    pre_order: 'pre_order',
    price: 'price'
    };

    程式碼解說:

    • 定義資料表的結構,把資料物件的屬性對應到資料庫的欄位。
    1. 建構函數
    constructor({ knexSql, tableName }: { knexSql: Knex; tableName?: string }) {
    super({ knexSql, tableName });
    }
    • 接受一個包含 knexSql 和可選 tableName 的物件作為參數,並呼叫父類別的建構函數super),將這些參數傳遞給父類別進行初始化。
    1. 靜態方法 createModel
    static createModel = ({
    knexSql,
    tableName,
    }: {
    knexSql: Knex;
    tableName?: string;
    }) => {
    return new ProductModel({ knexSql, tableName })
    }

    程式碼解說:

    • static:表示這是一個靜態方法,可以直接使用 ProductModel.createModel() 來調用。
    • createModel:方法名稱。
    • 參數:接受一個包含 knexSql 和可選 tableName 的物件。
    • 返回:創建並返回一個 ProductModel 的新實例,也就無需每次手動傳入參數來初始化它。
    分享至
    成為作者繼續創作的動力吧!
    © 2024 vocus All rights reserved.