Node.js 的ORM(Sequelize) 的使用

  Sequelize是一个Node.js 的ORM。ORM是对象关系映射(Object Relational Mapping),编程语言的中对象与关系型数据库中的关系(表)进行映射,对象的属性和值映射成表中的列和值。有了ORM,就可以使用面向对象的方式(调用对象的方法)来操作数据库,不用再写SQL语句。登录MySQL,CREATE DATABASE airline; 创建 airline 数据库。mkdir airline && cd airline && npm init -y,npm install sequelize mysql2,就可以使用Sequelize来操作数据库了。touch server.mjs,先连接数据库,就是创建Sequelize的实例

import { Sequelize } from 'sequelize';
// new Sequelize(数据库名, 登录数据库的用户名, 登录数据库的密码, {host: 哪台主机上的数据库, dialect: 使用什么数据库})
const sequelize = new Sequelize('airline', 'root', '123', {
    host: 'localhost',
    dialect: 'mysql'
});

try {
    await sequelize.authenticate();
    console.log('连接成功');
} catch (error) {
    console.error('连接失败', error);
}

  node server.js,连接成功,开始操作数据库。Sequelize有一个Model的概念,它代表数据库中的一张表,所以操作数据库,就要先创建Model,提供表的名称,表的列及其数据类型,从而和数据库表进行关联,这样操作Model就相当于操作表。两种方式创建Model,sequelize.define()和继承Model并调用 init()方法。

import { Sequelize, DataTypes } from 'sequelize';
// 参数: model名, 表中的字段及其数据类型, 可选配置项
sequelize.define('FlightSchedule', {
    originAirport: DataTypes.STRING,
    destinationAirport: DataTypes.STRING,
    departureTime: DataTypes.DATE
});

// 或者 类名就是model名
class FlightSchedule extends Model { }

// int参数:属性(对应表中的字段), 配置项
FlightSchedule.init({
    originAirport: DataTypes.STRING,
    destinationAirport: DataTypes.STRING,
    departureTime: DataTypes.DATE
}, {
    sequelize,
});

  创建Model并没有提供表名,默认情况下,Sequelize会把Model名进行复数化,当作表名。FlightSchedule就代表flightschedules表。当然,表名可以在可选配置项中进行配置,也可以禁止复数化,数据库的字段名也可以不用驼峰命名,而是使用 _连接,

{
    freezeTableName: true, // 表名是flightschedule
    tableName: 'a', // 直接定义表名为a
    underscored: true // 数据库中的对应的字段是origin_airport
}

  这里就不配置了,使用默认值就好。但此时数据库中并没有flightschedules表,怎么操作它?Sequelize提供了一个sync()方法,如果数据库中没有对应的表,它就会创建表,如果有,什么都不做。在define调用的后面

await sequelize.sync();

  node server.js ,airline有了flightschedules表,但多了3个字段,id,createdAt和updatedAt。默认情况下,Sequelize会为Model增加三个属性(id, createdAt和updatedAt)。创建model时写了三个属性,实际上model有六个属性。当然createdAt和updatedAt 是可以配置的(在可选配置项里面),timestamps为false,就不会给model上添加createdAt 和updatedAt属性。也可以添加某一个字段,比如timestamps为true, createdAt 为true,就只为model 增加createdAt属性。

   作为演示代码,以上使用没有问题。但真正的应用开发,就有很多问题,首先,数据库的配置信息写到主文件中,不利于安全,至少要写到配置文件中。其次,项目中有多个model,主文件就太大了,需要一个目录来维护model。再就是sync()方法,修改表就无能为力了,需要migiration。Sequlize 提供了sequelize-cli 来初始化项目结构。npm install sequelize-cli -D, 再npx sequelize init ,项目多了4个目录。config保存数据库配置信息,models包含项目中的所有Model。它默认包含index.js文件,从models目录下读取文件(fs.readdirSync(__dirname)),然后循环创建model(require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);),赋值给db对象(db[model.name] = model;),最后把db对象暴露出去。暂时不用它的,删除它。在models下创建flightSchedule.mjs,

export default (sequelize, DataTypes) => {
  return sequelize.define('FlightSchedule', {
    originAirport: DataTypes.STRING,
    destinationAirport: DataTypes.STRING,
    departureTime: DataTypes.DATE
  });
};

  然后在index.mjs

import { Sequelize, DataTypes } from 'sequelize';
import { createRequire } from 'module'; // 在esm中使用require,处理JSON

import createFlightSchedule from './flightSchedule.mjs' // 引入model

const env = process.env.NODE_ENV || 'development';
const require = createRequire(import.meta.url);
const config = require('../config/config.json')[env]

const sequelize = new Sequelize(config.database, config.username, config.password, config);

const FlightSchedule = createFlightSchedule(sequelize, DataTypes) // 创建Model

export {
  FlightSchedule,
  sequelize
}

  可以看到,默认情况下,连接的是development数据库,在config.json中配置development

"development": {
    "username": "root",
    "password": "123",
    "database": "airline",
    "host": "127.0.0.1",
    "dialect": "mysql"
}

  migrations目录包含对表Schema的定义和修改。创建了FlightSchedule Model,就要创建flightschedules表。新建一个建表migration,npx sequelize migration:generate --name create-flight-schedule,name参数表明这是什么migration。migrations目录下生成了一个文件,名字带有时间戳。它有两个方法,up(…) 用来定义migration要做的事情,down(…) 回滚这个migration做的事情,所以要在up中创建table,在down中删除table。需要注意的是,Sequelize默认会为Model增加id,createdAt和updatedAt属性,所以创建表时,表也要有对应的列

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.createTable('flightSchedules', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      originAirport: {
        type: Sequelize.STRING
      },
      destinationAirport: {
        type: Sequelize.STRING
      },
      departureTime: {
        type: Sequelize.DATE
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  async down(queryInterface, Sequelize) {
    await queryInterface.dropTable('flightSchedules');
  }
};

  在MySQL中删除flightschedules表。再npx sequelize db:migrate 执行migrations目录下的迁移操作,查看数据库,有了flightschedules表,但也多了SequelizeMeta表(记录执行过的迁移脚本)。执行migrate的时候,Sequelize会按时间戳的顺序遍历整个migrations目录, 然后跳过SequelizeMeta 表中包含的文件,也就是以前执行过的migration文件,不用再重复执行了。

  seeders目录快速向数据库中填充数据,方便测试程序。npx sequelize seed:generate --name initial-flight-schedules. seeders目录创建了一个js文件,up(…) 填充数据,down(…) 回滚数据。

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.bulkInsert('flightSchedules', [{
      originAirport: "济南",
      destinationAirport: "武汉",
      departureTime: "2022-01-01 08:00:00",
      createdAt: new Date(),
      updatedAt: new Date(),
    }], {});
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.bulkDelete('flightSchedules', null, {});
  }
};

  npx sequelize db:seed:all,执行seeders目录下的所有文件。npx sequelize db:seed --seed=fileName,指定执行seeders目录下哪个文件。无论执行哪一个命令,flightschedules表中都有一条记录。需要注意的是, db:migrate and db:seed 命令使用 NODE_ENV 环境变量来决定执行到哪个数据库,默认是development 配置下的数据库

  再创建一个Airplane Model,在models下新建airplane.mjs

export default (sequelize, DataTypes) => {
    return sequelize.define('Airplane', {
        planeModel: DataTypes.STRING,
        totalSeats: DataTypes.STRING,
    });
};

  再在index.mjs 引入并创建Airplane Model

import createAirplane from './airplane.mjs';

const Airplane = createAirplane(sequelize, DataTypes)

export {
  Airplane,
  FlightSchedule,
  sequelize
}

  创建migration,npx sequelize migration:generate --name create-airplane, 

module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.createTable('airplanes', {
      // id, createdAt和updatedAt都一样,这里就省略,没有写了
      planeModel: {
        type: Sequelize.STRING
      },
      totalSeats: {
        type: Sequelize.INTEGER
      },
    });
  },
  async down(queryInterface, Sequelize) {
    await queryInterface.dropTable('airplanes');
  }
};

  npx sequelize db:migrate 执行migration,再npx sequelize seed:generate --name initial-airplanes 建一个seeder文件,填充数据

module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.bulkInsert('airplanes', [{
      planeModel: 'Airbus A220-100',
      totalSeats: 110,
      createdAt: new Date(),
      updatedAt: new Date()
    }, {
      planeModel: 'Airbus A220-300',
      totalSeats: 110,
      createdAt: new Date(),
      updatedAt: new Date()
    }, {
      planeModel: 'Airbus A 318',
      totalSeats: 115,
      createdAt: new Date(),
      updatedAt: new Date()
    }, {
      planeModel: 'Boeing 707-100',
      totalSeats: 100,
      createdAt: new Date(),
      updatedAt: new Date(),
    }, {
      planeModel: 'Boeing 737-100',
      totalSeats: 85,
      createdAt: new Date(),
      updatedAt: new Date()
    }], {});
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.bulkDelete('airplanes', null, {});
  }
};

  npx sequelize db:seed --seed=20231017074324-initial-airplanes.js(需要改成自己的文件名)。再创建一个customer Model,在models目录下,创建customer.mjs 

export default (sequelize, DataTypes) => {
    return sequelize.define('Customer', {
        name: DataTypes.STRING,
        email: DataTypes.STRING,
    });
}

  然后index.mjs中引入

import createCustomer from './customer.mjs';

const Customer = createCustomer(sequelize, DataTypes);

export {
  Airplane,
  Customer,
  FlightSchedule,
  sequelize
}

  最后生成一个migration文件,npx sequelize migration:generate --name create-customer

module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.createTable('customers', {
      // 还是省略了id,createdAt和updateAt
      name: {
        type: Sequelize.STRING
      },
      email: {
        type: Sequelize.STRING
      },
    });
  },

  async down(queryInterface, Sequelize) {
    queryInterface.dropTable('customers');
  }
};

  npx sequelize db:migrate执行migration。现在server.mjs

import { sequelize, Customer, Airplane, FlightSchedule } from "./models/index.mjs";
await sequelize.authenticate()

  增删改查

  新增一条记录:Model名.create(),参数是一个对象,对象的属性就是创建model时定义的属性,Sequelize自动添加的属性,它自己会处理好。返回值是一个model实例,就是插入的表的一行,它有toJSON() 方法

const record = await Customer.create({name: 'Sam', email: '123@qq.com'});
console.log(record.toJSON())

  node server.mjs, 数据库中成功插入一条数据。这时可以npm i nodemon -D, npx nodemon  server.jms启动服务器。model名.bulkCreate()批量插入数据,它接受的是数组,数组的每一个元素是对象,对象和create()接受的对象一样

const record = await Customer.bulkCreate([
    { name: '张三', email: '456@qq.com' },
    { name: '李四', email: '789@qq.com' }, { name: '王五', email: '1789@qq.com' }
])
console.log(record.map(r => r.toJSON()))

  删除:如果只是删除一条记录,可以先model名.findOne() 查出这个实例, 然后再在实例上调用destroy()方法

var record = await Customer.findOne({ where: { id: 3 } });
await record.destroy();

  一次删除多条记录,那就要用model名.destroy(),destroy的参数就是筛选条件。 

await Customer.destroy({ where: { id: 2 } });

  删除还有硬删除和软删除之分。创建model时配置了paranoid: true,就表示软删除,删除只是在model实例上添加deleteAt字段,不会真正的从数据库中删除数据,当然前提是timestamps 设为true,此时要硬删除,就需要在调用destroy方法时,添加force: true。如果软删除,在migration的时候,要在表中创建deleteAt字段。

await Customer.destroy({ where: { id: 4 }, force: true });

  更新:更新多条记录,使用model名.update(),第一个参数是要更新成什么, 第二个参数是查询条件

await Customer.update(
    { name: '张三', email: '456@qq.com' },
    { where: { id: 1 } }
);

  如果只想更新一条记录,先调用model名.findOne()获取到实例,然后修改实例的属性或调用increment 和decrement 方法修改属性,最后调用实例上的save()。

var record = await Customer.findOne({ where: { id: 1 } });
record.name = 'Sam'
await record.save()

 查询:modal名.方法名,比如findAll,findone。

const airplances = await Airplane.findAll(); // findAll返回的是数组。
console.log(airplances.map(ap => ap.toJSON()));
const airplance = await Airplane.findOne();
console.log(airplance.toJSON())

  查询方法中,参数带有attributes,是个数组,列出要查询的表中的某个或某些字段,而不是全部字段。

app.get('/', async (req, res) => {
    const airplances = await models.Airplane.findAll({
        attributes: ['planeModel', 'totalSeats']
    });
    res.send("<pre>" + JSON.stringify(airplances, undefined,
        4) + "</pre>");
})

  如果对字段进行重命名,数组中的元素需要是一个数组,它的第一项是原字段名,第二项是新字段名。

const airplances = await models.Airplane.findAll({
        attributes: [
            'planeModel',
            ['totalSeats', 'seats'], // totalSeats 重命名为 seats
        ]
 });

  查询方法中,参数中带有where是条件查询,为此Sequelize还专门提供了Op。

import { Op } from 'sequelize';

app.get('/', async (req, res) => {
    const airplances = await models.Airplane.findAll({
        where: {
            id: {
                [Op.or]: [1, 2]
            }
        }
    });
    res.send("<pre>" + JSON.stringify(airplances, undefined,
        4) + "</pre>");
})

  

  如果SQL语句中有聚合函数,要用sequelize.fn 来定义函数,sequelize.col来指定对哪一个属性来进行函数操作。sequelize.where 来表示相等性。attributes 还有 exclude;attributes:{exclude:['password']}

const airplances = await models.Airplane.findAll({
        where: models.sequelize.where(
            models.sequelize.fn('char_length', models.sequelize.col('planeModel')),
            5)
});

  sequelize.where 的第二个参数是基本类型,它就进行相等比较。如果要进行其它比较,可以是个对象,对象的属性,就果定义什么比较

app.get('/', async (req, res) => {
    const airplances = await models.Airplane.findAll({
        where: models.sequelize.where(
            models.sequelize.fn('char_length', models.sequelize.col('planeModel')),
            {
                [Op.gt]: 12
            })
    });
    res.send("<pre>" + JSON.stringify(airplances, undefined,
        4) + "</pre>");
})

  查询方法中,带有order,就是排序,它是一个数组,数组的每一项也是一个数组,数组的第一项指定按哪个字段进行排序,第二项指定按升序还是降序进行排序

app.get('/', async (req, res) => {
    const airplances = await models.Airplane.findAll({
       order: [
           ['planeModel', 'DESC']
       ]
    });
    res.send("<pre>" + JSON.stringify(airplances, undefined,
        4) + "</pre>");
})

  查询方法中,带有group,就是分组,指定按哪一个字段进行分组。分组之后,通常使用聚合函数,聚合函数的实现是使用sequelize.fn,它的第一个参数是使用哪个聚合函数(字符串),第二个参数是对哪个字段进行聚合。通常聚合函数要进行重命名,所以聚合函数是数组的第一项,新的名字是第二项,然后整个数组放到attributes数组中,

app.get('/', async (req, res) => {
    const airplances = await models.Airplane.findAll({
        attributes: ['planeModel', [models.sequelize.fn('sum', models.sequelize.col('totalSeats')), 'totalSeatsCount']],
        group: 'planeModel'
    });
    res.send("<pre>" + JSON.stringify(airplances, undefined,
        4) + "</pre>");
})

  查询方法中,带有offset和limit,就是用于分页

app.get('/', async (req, res) => {
    const airplances = await models.Airplane.findAll({
        offset: 1, limit: 2
    });
    res.send("<pre>" + JSON.stringify(airplances, undefined,
        4) + "</pre>");
})

  在model的定义中,还有setter,getter。getter,对从数据库获取回来的数据,进行转化,比如,小写转化成大写。setter 是在存储到数据库之前对数据进行转化,比如 对密码进行加密。this.getDataValue, this.setDataValue。虚拟字段,不存数据库,sequlize自己做的转化,主要作用是组合不同的属性,它的类型是DataType.VIRTUAl,然后设置get方法,返回值。

  sequlieze.query 可以执行原生sql。

  关系

  在关系型数据库中,表与表之间存在1对1,1对多,和多对多的关系,那在Sequelize中,怎么用model来表示这些关系?每一个model都有四个方法,hasOne, BelongsTo, hasMany, BelongsToMany。由于关系是相对的,所以每一种关系都用两个方法实现。 

  实现1对1,用hasOne和BelongsTo。比如飞机(Airplanes)和飞机详情表(AirplaneDetails), 由于表与表之间的关系是通过外键实现的,外键放到哪张表上?哪张表能单独存在,airplanes能单独存在,details则是依附airplane的,所以外键放到details上,airplanes有一个(hasOne)details, details属于(BelongsTo)airplane,

Airplane.hasOne(AirplaneDetail)
AirplaneDetail.BelongsTo(Airplane)

  hasOne把外键放到它的参数上,AirplaneDetail model中,多了一个AirplaneId属性,它所引用的model名+Id,相应的,airplaneDetail 表多了AirplaneId列,除非AirplaneId列已存在。当有了关系后,调用hasOne和BelongsTo的model 实例多了几个辅助方法,来方便的设置关系。从数据库查到airplane后,airplane有getAirplaneDetail, setAirplaneDetail 和createAirplaneDetail 方法。AirplaneDetail实例setAirplane等方法。

  实现1对多,用hasMany 和belongsTo。比如 一架飞机可以执行多次航班(hasMany),但一个航班只能用一架飞机(BelongsTo)。

Airplane.hasMany(FlightSchedule)
FlightSchedule.belongsTo(Airplane)

  1对多的关系,外键放到多的那一边,所以FlightSchedule多了AirplaneId属性。实现多对多要用BelongsToMany,因为多对多的关系,需要中间表,所以它还要一个through参数来指定使用哪张表,如果参数是字符串,Sequelize默认表中字段名是两个关联model的名字+id。比如Customer 和FlightSchedule,一个乘客可以乘多趟航班,一趟航班有多个客人,它们之间的关联是购买的飞机票

Customer.belongsToMany(FlightSchedule, { through: 'BoardingTickets' });
FlightSchedule.belongsToMany(Customer, { through: 'BoardingTickets' });

  默认情况下,Sequelize会操作BoardingTickets表,表中有FlightScheduleId和CustomerId字段。参数还可以一个model,

const BoardingTickets = sequelize.define('BoardingTickets', {
  FlightScheduleId: {
    type: DataTypes.INTEGER,
    references: {
      model: FlightSchedule,
      key: 'id'
    }
  },
  CustomerId: {
    type: DataTypes.INTEGER,
    references: {
      model: 'Customers', // 字符串是指表名
      key: 'id'
    }
  },
  // SomeOtherColumn: { // 可以添加其它字段
  //     type: DataTypes.STRING
  // }
});

Customer.belongsToMany(FlightSchedule, { through: 'BoardingTickets' });
FlightSchedule.belongsToMany(Customer, { through: 'BoardingTickets' });

  两种方式是一样的,在models的index.js下面,在 export default前面,

Airplane.hasMany(FlightSchedule)
FlightSchedule.belongsTo(Airplane)

Customer.belongsToMany(FlightSchedule, { through: 'BoardingTickets' });
FlightSchedule.belongsToMany(Customer, { through: 'BoardingTickets' });

  为了满足关系,需要做migration,首先创建BoardingTickets,  npx sequelize migration:generate --name create-board-ticket

module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.createTable('BoardingTickets', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      CustomerId: {
        type: Sequelize.INTEGER,
        references: {
          model: 'Customers',
          key: 'id'
        },
        onUpdate: 'set null',
        onDelete: 'cascade'
      },
      FlightScheduleId: {
        type: Sequelize.INTEGER,
        references: {
          model: 'FlightSchedules',
          field: 'id'
        },
        onDelete: 'set null',
        onUpdate: 'cascade'
      }
    });
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.dropTable('BoardingTickets');
  }
};

  然后给FlightSchedules 添加外键,npx sequelize migration:generate --name add-flightschedules-references

module.exports = {
  async up (queryInterface, Sequelize) {
    await queryInterface.addColumn('FlightSchedules', 'AirplaneId', {
      type: Sequelize.INTEGER,
    });

    await queryInterface.addConstraint('FlightSchedules', {
      type: 'foreign key',
      fields: ['AirplaneId'],
      references: {
        table: 'Airplanes',
        field: 'id'
      },
      name: 'fkey_flight_schedules_airplane',
      onDelete: 'set null',
      onUpdate: 'cascade'
    });
  },

  async down (queryInterface, Sequelize) {
    await queryInterface.removeConstraint(
      'FlightSchedules', 'fkey_flight_schedules_airplane'
    );

    await queryInterface.removeColumn('FlightSchedules', 'AirplaneId');
  }
};

  执行迁移

sequelize db:migrate

   当model之间建立联系后,每一个model实例多了许多方法,可以使用一个model实例去操作另外一个model。比如找到一个airplane,就可以找到相对应的flight schedue,所以airplane的实例,就有get fligtShcedule和addflightSchedule,createFlightShcedule,addaddflightSchedules, countFlightSchedules 等方法

app.get('/', async (req, res) => {
    const airplance = await models.Airplane.findOne({
        where: { id: 1 }
    });

    const flightSchedule = await models.FlightSchedule.findOne({
        where: { id: 1 }
    })
    // 数据库中 flightSchedule表中,id=1的 AirplaneId 变成了 '1'
    await airplance.addFlightSchedule(flightSchedule)
    // 创建了一条flightSchedule记录,并且它的AirplaneId也是1
    await airplance.createFlightSchedule({
        originAirport: "深圳",
        destinationAirport: "武汉",
        departureTime: "2023-10-01 20:00:00"
    })

    const flightSchedules = await models.FlightSchedule.findAll();
    res.send("<pre>" + JSON.stringify(flightSchedules, undefined,
        4) + "</pre>");
})

  当model之间的关系是多对多,在一个model实例上执行get另一个model时,会把中间表也查出来 

app.get('/', async (req, res) => {
    const customer = await models.Customer.findOne({
        where: { id: 1 }
    });

    const flightSchedules = await customer.getFlightSchedules();
    res.send("<pre>" + JSON.stringify(flightSchedules, undefined,
        4) + "</pre>");
})

  页面展示:

[
    {
        "id": 1,
        "originAirport": "济南",
        "destinationAirport": "武汉",
        "departureTime": "2022-01-01T08:00:00.000Z",
        "createdAt": "2023-10-21T15:05:37.000Z",
        "updatedAt": "2023-10-23T14:44:16.000Z",
        "AirplaneId": 1,
        "BoardingTickets": {
            "createdAt": "2023-10-23T15:00:55.000Z",
            "updatedAt": "2023-10-23T15:00:55.000Z",
            "CustomerId": 1,
            "FlightScheduleId": 1
        }
    }
]

  如果不想返回中间表,或只想返回中间表的某些属性,get方法中,可以使用joinTableAttributes来进行指定,

 const flightSchedules = await customer.getFlightSchedules({
        joinTableAttributes: []
 });

  以上的查询称为 Lazy load, 先从一张表中查出数据,再从另外一张表中,查出数据,执行了两次query请求,那能不能一次性地把所有数据都查询出来,那就是Eager load,查询的时候,使用include,包含关联的表,连表查询。

app.get('/', async (req, res) => {
    const customer = await models.Customer.findOne({
        where: { id: 1 },
        include: [{
            model: models.FlightSchedule, //连接FlightSchedules 表
            through: { attributes: [] } // 不需要中间表的数据
          }]
    });

    res.send("<pre>" + JSON.stringify(customer, undefined,
        4) + "</pre>");
})

  一次查出了customer和它关联的 FlightSchedules。如果不使用默认外键,也可以自己定义,甚至外键不引用Id,引用其它字段,hasOne

Actor.hasOne(Role, {
    sourceKey: 'name',
    foreignKey: 'actorName'
});

  Role model多了一个actorName属性 ,因为hasOne定义外键在Role上,它的值引用ActorModel中的name属性,hasMany也是一样

Roles.hasMany(Costumes, {
    sourceKey: 'title',
    foreignKey: 'roleTitle'
});

  Costumes model 多了roleTitle属性, 它的值引用which will be associated with the role’s title。 belonsTo:

Roles.belongsTo(Actors, {
    targetKey: 'name',
    foreignKey: 'actorName'
});

  belongsToMany

Costumes.belongsToMany(Actors, {
    through: 'actor_costumes',
    sourceKey: 'name',
    targetKey: 'wardrobe'
});

  actor_costumes 中间表建立,两个字段CostumesName 和 ActorWardrobe, 分别引用Actor 和 Costume model.

  事务:分为 unmanaged 事务和managed事务。 unmanaged事务,手动创建事务,提交或回滚事物 

app.get('/', async (req, res) => {
    const tx = await models.sequelize.transaction(); // 创建一个事务
    try {
        const plane = await models.Airplane.findByPk(2);

        const schedule = await models.FlightSchedule.create({
            originAirport: '上海',
            destinationAirport: '北京',
            departureTime: '2023-10-24 10:10:00',
        }, { transaction: tx });

        await schedule.setAirplane(plane, { transaction: tx });

        await tx.commit(); // 提交事务

        res.send("<pre>" + JSON.stringify(schedule, undefined,
            4) + "</pre>");
    } catch (error) {
         await tx.rollback(); // 回滚事务
    }
})

  managed transactions 就是自动提交和回滚事务。把要做的事性作为回调函数,传递给sequelize.transaction 就是managed transactions 

app.get('/', async (req, res) => {
    try {
        const plane = await models.Airplane.findByPk(3);

        const flight = await models.sequelize.transaction(async (tx) => {
            const schedule = await models.FlightSchedule.create({
                originAirport: '上海',
                destinationAirport: '深圳',
                departureTime: '2023-10-24 10:10:00',
            }, { transaction: tx });

            await schedule.setAirplane(plane, { transaction: tx });

            return schedule
        })

        // 在这里自动提交事务
        res.send("<pre>" + JSON.stringify(flight, undefined,
            4) + "</pre>");
    } catch (error) {
          //在这里,事务已经回滚了
    }
})

   日志: 当执行数据库操作时,默认会把执行的query在控制台打印出来,Sequelize 同时提供了几种不同的日志签名,就是函数签名,来实现自定义日志。

function (msg) {}
function (...msg) {}
msg => someLogger.debug(msg)

  function (...msg) {}, 不仅记录query,还会记录其它元信息

function multiLog(...msgs) {
    msgs.forEach(function(msg) {
        console.log(msg);
    });
}
const sequelize = new Sequelize('sqlite::memory:', {
    logging: multiLog
});

  msg => someLogger.debug(msg) 可以让我们集成第三方日志库,比如Pino

import pino from 'pino'
const logger = pino();

const sequelize = new Sequelize('sqlite::memory:', {
    logging: (msg) => logger.info(msg)
});

  再比如集成Bunyan

import bunyan from 'bunyan'
const logger = bunyan.createLogger({name: 'app'});

const sequelize = new Sequelize('sqlite::memory:', {
    logging: (msg) => logger.info(msg)
});

   当然,也可以禁止输出日志,logging: false就可以了。

  收集metrics和数据是使用 OpenTelemetry, 这个暂时记录一下

npm i @opentelemetry/api @opentelemetry/sdk-trace-node @
opentelemetry/instrumentation @opentelemetry/sdk-node @
opentelemetry/auto-instrumentations-node opentelemetry-
instrumentation-sequelize

  在models的index.js中

const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { SequelizeInstrumentation } = require('opentelemetry-instrumentation-sequelize');

const tracerProvider = new NodeTracerProvider({
  plugins: {
    sequelize: {
      // disabling the default/old plugin is required
      enabled: false,
      path: 'opentelemetry-plugin-sequelize'
    }
  }
});

registerInstrumentations({
  tracerProvider,
  instrumentations: [
    new SequelizeInstrumentation({
      // any custom instrument options here
    })
  ]
});

  在项目根目录下,创建trace.js

/* tracing.js */

// Require dependencies
const opentelemetry = require("@opentelemetry/sdk-node");
const { getNodeAutoInstrumentations } = require("@opentelemetry/auto-instrumentations-node");

const sdk = new opentelemetry.NodeSDK({
  traceExporter: new opentelemetry.tracing.ConsoleSpanExporter(),
  instrumentations: [getNodeAutoInstrumentations()]
});

sdk.start();

  node -r "./tracing.js" index.js 启动服务器,访问服务器,就可以看到日志输出。通常会把metrics数据放到一个收集器中,比如Zipkin, 在models 下的index.js  引入

import { ZipkinExporter } from '@opentelemetry/exporter-zipkin';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';

  然后在const tracerProvider = new NodeTracerProvider({}) 下面写

tracerProvider.addSpanProcessor(new BatchSpanProcessor(new
    ZipkinExporter()));

  对已经存在的数据库使用Sequelize,由于数据库已经存在,我们需要创建model来适配它的Schema。比如数据库有一张表是foo_bars, 它的属性是

id INTEGER AUTOINCREMENT
first_name VARCHAR(200)
last_name VARCHAR(200)
email VARCHAR(200)
date_created DATETIME
date_updated DATETIME

  没有createdAt 和UpdatedAt, 并且表名和字段名都使用下划线,所以在定义model的时候,都需要自定义

{
      // options
      sequelize,
      modelName: 'FooBar',
      tableName: 'foo_bars',
      createdAt: 'date_created',
      updatedAt: 'date_updated',
      underscore: true,
    },

  第二个要注意的是如果没有表中没有使用id作为主键,需要在创建的model中删除id,

class FooBar extends Model {}
FooBar.removeAttribute('id');

 

posted @ 2023-10-25 21:35  SamWeb  阅读(461)  评论(0编辑  收藏  举报