NodeJS 的Express 中集成 MySQL 和 Knex 的完整指南(二)

222 阅读3分钟

NodeJS 的Express 中集成 MySQL 和 Knex 的完整指南(一)中的增删改查案例里,有在创建users表时没有添加的status字段,本文记录Knex怎么在已有表添加新字段以及添加多个新表的操作:


步骤 1:创建迁移文件

npx knex migrate:make add_status_to_users  # 添加 status 字段到 users
npx knex migrate:make create_tables        # 创建新表 orders/products/categories

步骤 2:更新 users 表(添加 status 字段)

文件:migrations/YYYYMMDDHHMMSS_add_status_to_users.js

exports.up = function (knex) {
  return knex.schema.alterTable('users', (table) => {
    table.enu('status', ['active', 'inactive']) // MySQL 使用 enu 方法
         .notNullable()
         .defaultTo('active');
  });
};

exports.down = function (knex) {
  return knex.schema.alterTable('users', (table) => {
    table.dropColumn('status');
  });
};

步骤 3:创建新表(无外键约束)

文件:migrations/YYYYMMDDHHMMSS_create_tables.js

exports.up = function (knex) {
  return knex.schema
    // 创建 categories 表
    .createTable('categories', (table) => {
      table.increments('id').primary();
      table.string('name', 255).notNullable().unique();
      table.text('description');
      table.timestamp('created_at').defaultTo(knex.fn.now());
      table.timestamp('updated_at').defaultTo(knex.fn.now());
    })
    // 创建 products 表(逻辑关联 categories)
    .createTable('products', (table) => {
      table.increments('id').primary();
      table.string('name', 255).notNullable();
      table.text('description');
      table.decimal('price', 10, 2).notNullable(); // 十进制类型
      table.integer('stock').unsigned().defaultTo(0);
      table.integer('category_id').unsigned(); // 仅字段关联,无外键约束
      table.timestamp('created_at').defaultTo(knex.fn.now());
      table.timestamp('updated_at').defaultTo(knex.fn.now());
    })
    // 创建 orders 表(逻辑关联 users)
    .createTable('orders', (table) => {
      table.increments('id').primary();
      table.integer('user_id').unsigned(); // 仅字段关联,无外键约束
      table.decimal('total_amount', 10, 2).notNullable();
      table.enu('status', ['pending', 'paid', 'shipped', 'cancelled'])
          .defaultTo('pending');
      table.timestamp('created_at').defaultTo(knex.fn.now());
      table.timestamp('updated_at').defaultTo(knex.fn.now());
    });
};

exports.down = function (knex) {
  return knex.schema
    .dropTable('orders')
    .dropTable('products')
    .dropTable('categories');
};

Knex 提供的字段类型函数如下:

  1. integer():整数
  2. float():浮点数
  3. decimal():十进制数
  4. boolean():布尔值
  5. date():日期
  6. dateTime():日期时间
  7. time():时间
  8. timestamp():时间戳
  9. string():字符串
  10. text():文本
  11. json():JSON
  12. binary():二进制数
  13. uuid():UUID

步骤 4:执行迁移

npx knex migrate:latest

最终表结构说明

1. users

字段名类型约束
idint主键、自增
namevarchar(255)非空
emailvarchar(255)非空、唯一
statusenum允许 'active'/'inactive',默认 'active'
created_attimestamp默认当前时间
updated_attimestamp默认当前时间

2. categories

字段名类型约束
idint主键、自增
namevarchar(255)非空、唯一
descriptiontext可选
created_attimestamp默认当前时间
updated_attimestamp默认当前时间

3. products

字段名类型约束
idint主键、自增
namevarchar(255)非空
descriptiontext可选
pricedecimal(10,2)非空
stockint unsigned非负、默认 0
category_idint unsigned逻辑关联分类
created_attimestamp默认当前时间
updated_attimestamp默认当前时间

4. orders

字段名类型约束
idint主键、自增
user_idint unsigned逻辑关联用户
total_amountdecimal(10,2)非空
statusenum允许 'pending'/'paid'/'shipped'/'cancelled',默认 'pending'
created_attimestamp默认当前时间
updated_attimestamp默认当前时间

关键说明

  1. 没有外键约束
    所有关联字段(如 user_idcategory_id)仅为普通整数字段,无 references 约束。

  2. MySQL 适配

    • 使用 enu() 替代 enum() 定义枚举字段。
    • 显式定义 timestamp 字段并设置默认值 knex.fn.now()
  3. 字段逻辑关联

    • products.category_idcategories.id 通过代码逻辑关联。
    • orders.user_idusers.id 通过代码逻辑关联。

操作验证

  1. 插入测试数据:

    // 示例:插入一个分类和关联商品
    const categoryId = await knex('categories').insert({ 
      name: '电子产品', 
      description: '手机、电脑等' 
    });
    
    await knex('products').insert({
      name: '智能手机',
      price: 2999.99,
      category_id: categoryId[0] // 手动关联分类
    });
    
  2. 查询订单及关联用户:

    const orders = await knex('orders')
      .select('orders.*', 'users.name as user_name')
      .leftJoin('users', 'orders.user_id', 'users.id'); // 手动关联查询
    

常见问题

Q: 如何确保关联数据的完整性?
A: 需在业务代码中校验关联数据是否存在(如插入订单前检查 user_id 是否有效)。

Q: 时间戳字段是否需要手动更新?
A: 在更新数据时需手动更新 updated_at

await knex('products')
  .where({ id: 1 })
  .update({ 
    price: 2599.99,
    updated_at: knex.fn.now() // 手动更新时间戳
  });

Q: 事物的提交和回滚

await db.transaction(async (trx) => {
    try {
        // 扣减库存
        await trx('products')
            .where('id', 1)
            .decrement('stock', 1);

        // 创建订单
        const [orderId] = await trx('orders')
            .insert({
                user_id: 1,
                total_amount: 99.99
            });
        await trx.commit() //提交事务
    } catch (err) {
        await trx.rollback() //回滚事务
    }
});

本文使用到的npx,在 npx 的详细说明 中进行了介绍