Sequelize是一个Node.js 的ORM。什么是ORM呢?对象关系映射(Object Relational Mapping)。什么意思?就是在编程语言中,很容易创建对象,如果在面向对象的语言中,它也只有对象,但在关系型数据库中,它只有关系(表)。如果想在程序中操作关系型数据库,就要在对象和表之间做转换,比如,取出对象中的属性值,写一个SQL语句,转化成表的属性的值。ORM就是做这层转换的,对象的属性和值,和表中的属性和值一一对应(映射),操作对象,就可以操作关系型数据库,不用写SQL语句。因此,学习Sequelize 要先有一个数据库,比如MySQL,再创建一个Node.js项目,然在项目中使用Sequelize操作MySQL数据库。打开命令行,登录MySQL,CREATE DATABASE airline; 创建 airline 数据库。mkdir airline && cd airline && npm init -y, npm install express,  新建server.js,

import express from 'express';
const app = express(); app.get('/', (req, res) => {
res.send('Hello World')
})
app.listen(3000, () => {
console.log("服务器启动成功");
})

  由于使用ES module,在package.json中,设置"type": "module",node server.js,项目启动成功,最简单的Node.js项目创建完成。npm install sequelize mysql2,就可以在项目中使用Sequelize来操作数据库了。操作数据库要先连接数据库,连接数据库就是创建Sequelize的实例, 在server.js中

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);
}

   连接成功,怎么操作数据库呢?Sequelize有一个model的概念,它代表数据库中的一张表,操作model就相当于操作数据库中的表,想要操作数据库哪张表,就要为哪张表创建一个model,因此使用Sequelize都是从创建model开始。有两种方式创建model,一种是sequelize.define(),另外一种是继承Model并调用 init()方法,无论哪种方式,创建Model时候,都要提供表名,以及表中的字段和数据类型,因为Model和表相关,代表的是一张表 

// 参数: model名, 属性(对应表中的字段), 可选配置项
sequelize.define('FlightSchedule', {
originAirport: DataTypes.STRING,
destinationAirport: DataTypes.STRING,
departureTime: DataTypes.DATE
}); // 或者
class FlightSchedule extends Model { } // int参数:属性(对应表中的字段), 配置项
FlightSchedule.init({
originAirport: DataTypes.STRING,
destinationAirport: DataTypes.STRING,
departureTime: DataTypes.DATE
}, {
sequelize,
});

  你可能已经注意到了,创建FlightSchedule并没有提供表名,这是因为在默认情况下,Sequelize会对model名进行复数化,当作表名。FlightSchedule就会操作flightschedules表。当然,可以提供表名,也可以禁止复数化,还有就是,数据库的字段名也可以不用驼峰命名,而是使用 _连接,这些都可以在可选配置项中进行配置,

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

  这里就不配置了,使用默认值就好,可以操作表了,但数据库中表不存在怎么办?Sequelize提供了一个sync()方法,如果数据库中没有对应的表,它就会创建表,如果有表,那就什么都不做,整个index.js如下

import express from 'express';
import { Sequelize, DataTypes } from 'sequelize'; const app = express(); const sequelize = new Sequelize('airline', 'root', '123', {
host: 'localhost',
dialect: 'mysql'
}); sequelize.define('FlightSchedule', {
originAirport: DataTypes.STRING,
destinationAirport: DataTypes.STRING,
departureTime: DataTypes.DATE
});
await sequelize.sync(); app.get('/', (req, res) => {
res.send('Hello World')
})
app.listen(3000, () => {
console.log("server start");
})

  重新启动服务器,查看MySQL数据库,airline下面多了flightschedules表,但也会发现flightschedules表多了3个字段,id,createdAt和updatedAt。默认情况下,Sequelize会为创建的model增加这三个属性,id对应表中的主键, createdAt和updatedAt属性来记录表中字段的创建和更新。创建model的时候,写了三个属性,实际上model有六个属性。当然 createdAt和updatedAt 是可以配置的,它们统称为 timestamps, timestamps为false,就不会给model上添加createdAt 和updatedAt字段。也可以添加某一个字段,比如timestamps为true, createdAt 为true,就只为model 增加createdAt属性。还可以给它们重命名,比如,createdAt: 'addAt'。

  作为演示代码,以上使用Sequelize的方法,没有问题。但作为真正的应用程序,它还是有很多问题的,首先,数据库的配置信息写到主文件中,不利于安全,因为都是用户名和密码,至少要写到配置文件中,其次,项目中model肯定很多,至少需要一个目录来维护model。再就是sync()方法,如果修改表,就无能为力了,这需要migiration。我们可以手动创建这些目录和文件,但Sequlize 提供了 sequelize-cli 命令行工具,可以更好地完成任务。npm install --save-dev sequelize-cli, 首先是npx sequelize init ,初始化Sequelize项目,多了4个目录,

  config目录:config.json保存数据库配置信息,可以配置开发环境,测试环境,和生产环境的数据库信息。

  models目录:包含项目中的所有Model。它默认包含index.js文件,从models目录下读取文件(fs.readdirSync(__dirname)),然后循环创建model(require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);),赋值给db对象(db[model.name] = model;),最后把db对象暴露出去。也就是说,可以一个文件定义一个model。在models下创建flightSchedule.js,把FlightSchedule的定义放到里面

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

  然后在index.js

import { Sequelize, DataTypes } from 'sequelize';
import { createRequire } from 'module'; // 在esm中使用require,处理JSON import createFlightSchedule from './flightSchedule.js' // 引入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 default {
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表,为此Sequelize提供了Query Interface和migration命令行工具。
使用npx sequelize migration:generate 就可以生成一个migration文件,不过要提供了 --name参数来定义这个migration,比如 npx sequelize migration:generate --name create-flight-schedule。migrations目录下生成了一个文件,文件名的格式是时间戳-action-表名。它有两个方法,up(…) 用来执行migration要做的事情,down(…) 回滚这个migration做的事情,所以要在up方法中创建table,在down方法中,删除table。需要注意的是,创建model的时候是3个属性,但是默认会增加Id,createdAt和updatedAt,所以model有6个属性,创建表的时候,表也要有对应的6个属性

/** @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');
}
};

  npx sequelize db:migrate 执行migrations目录下的数据库的迁移操作,但是报错了,因为sequelize命令并不支持ES module,所以把migration文件的后缀改为.cjs, 再次执行npx sequelize db:migrate ,查看MySQL数据库,有了flightschedules表, 但也多了SequelizeMeta 表(记录执行过的迁移脚本)。执行migrate的时候,Sequelize会按时间戳的顺序遍历整个migrations目录, 然后跳过 SequelizeMeta 表中包含的文件,也就是以前执行过的migration文件,不用再重复执行了。

   seeders目录:快速向数据库中填充数据,方便测试程序。使用npx sequelize seed:generate生成文件,它也是有一个--name,给这个seed操作起个名字,npx sequelize seed:generate --name initial-flight-schedules. 在seeders目录创建了一个js文件,还是把它变成.cjs文件,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 配置下的数据库

  再创建一个model,比如Airplane,熟悉一下流程。先在models下新建airplane.js

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

  再在其index.js 引入并创建Airplane model

import createAirplane from './airplane.js';

const Airplane = createAirplane(sequelize, DataTypes)

export default {
Airplane,
FlightSchedule,
sequelize
}

  创建migration, npx sequelize migration:generate --name create-airplane, 不要忘了把生成的文件后缀名改为.cjs,

module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Airplanes', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
planeModel: {
type: Sequelize.STRING
},
totalSeats: {
type: Sequelize.INTEGER
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Airplanes');
}
};

   npx sequelize db:migrate 执行migration,可以再创建一个seeder文件,填充数据库中的数据。npx sequelize seed:generate --name initial-airplanes

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.cjs 向数据库中插入数据。再创建一个customer,在models目录下,创建customer.js

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

  然后index.js中引入

import createCustomer from './customer.js';

const Customer = createCustomer(sequelize, DataTypes);

export default {
Airplane,
Customer,
FlightSchedule,
sequelize
}

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

module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Customers', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING
},
email: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
}, async down(queryInterface, Sequelize) {
queryInterface.dropTable('Customers');
}
};

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

import express from 'express';
import models from './models/index.js' // 引入model const app = express(); // 由于执行mirgation,只要成功连接数据库,就能操作数据库,表一定会存在
await models.sequelize.authenticate(); app.get('/', (req, res) => {
res.send('Hello World')
})
app.listen(3000, () => {
console.log("server start");
})

  增删改查

  新增一条记录:model名.create(),它接受一个对象作为参数,对象的属性就是model中定义的属性,返回一个model的实例,

app.get('/', async (req, res) => {
var record = await models.Customer.create({name: 'Sam', email: '123@qq.com'});
res.send(JSON.stringify(record))
})

  启动服务器,localhost:3000,数据库中成功插入一条数据,同时页面显示新创建的customer。create()创建model实例的时候,只需要操作创建Model时定义的属性,Sequelize自动添加的属性,它自己会处理好。这时可以npm i nodemon -D, npx nodemon  server.js启动服务器,保证服务器一直处于启动状态。新增多条记录(批量插入):model名.bulkCreate(),它接受的是数组,数组的每一个元素是对象,对象和create()接受的对象一样

app.get('/', async (req, res) => {
var record = await models.Customer.bulkCreate([
{name: '张三', email: '456@qq.com'},
{name: '李四', email: '789@qq.com'},
])
res.send(JSON.stringify(record))
})

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

app.get('/', async (req, res) => {
var record = await models.Customer.findOne({ where: { id: 3 } });
await record.destroy();
res.send(JSON.stringify(record))
})

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

app.get('/', async (req, res) => {
var record = await models.Customer.destroy({ where: { id: 2 } });
res.send(JSON.stringify(record))
})

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

await Post.destroy({
where: {
id: 1
},
force: true
});

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

app.get('/', async (req, res) => {
var record = await models.Customer.update(
{ name: '张三', email: '456@qq.com' },
{ where: { id: 1 } }
);
res.send(JSON.stringify(record))
})

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

app.get('/', async (req, res) => {
var record = await models.Customer.findOne(
{ where: { id: 1 } }
);
record.name = 'Sam'
await record.save() res.send(JSON.stringify(record))
})

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

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

  查询方法中,参数中带有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>");
})

  如果where查询语句中调用函数,要用sequelize.fn 来定义函数,sequelize.col来指定对哪一个属性来进行函数操作。sequelize.where 来表示相等性。

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>");
})

  关系

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

  实现1对1,用hasOne和BelongsTo。比如飞机(Airplanes)和飞机详情表(AirplaneDetails)

Airplanes.hasOne(AirplaneDetails)
AirplaneDetails.BelongsTo(Airplanes)

  由于表与表之间的关系是通过外键实现的,所以默认情况下,hasOne方法会把外键放到它的参数上,也就是AirplaneDetails model中,外键的列名它所引用的model名+Id,AirplaneId, 除非外键已经存在,AirplaneDetails model 多了一个AirplaneId属性,

  实现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 fligt shcedule 的方法。当有一个airplane时,add一个flight schedule,两者就建立了联系,甚至,一个airplane 都有create flight shcedule 的功能,创建和建立联系一起作了。

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');

Node.js 的ORM(Sequelize) 的使用的更多相关文章

  1. 用node.js实现ORM的一种思路

    ORM是O和R的映射.O代表面向对象,R代表关系型数据库.二者有相似之处同时也各有特色.就是因为这种即是又非的情况,才需要做映射的. 理想情况是,根据关系型数据库(含业务需求)的特点来设计数据库.同时 ...

  2. 在node.js中,使用基于ORM架构的Sequelize,操作mysql数据库之增删改查

    Sequelize是一个基于promise的关系型数据库ORM框架,这个库完全采用JavaScript开发并且能够用在Node.JS环境中,易于使用,支持多SQL方言(dialect),.它当前支持M ...

  3. [转]在node.js中,使用基于ORM架构的Sequelize,操作mysql数据库之增删改查

    本文转自:https://www.cnblogs.com/kongxianghai/p/5582661.html Sequelize是一个基于promise的关系型数据库ORM框架,这个库完全采用Ja ...

  4. node+pm2+express+mysql+sequelize来搭建网站和写接口

    前面的话:在这里已经提到了安装node的方法,node是自带npm的.我在技术中会用es6去编写,然后下面会分别介绍node.pm2.express.mysql.sequelize.有少部分是摘抄大佬 ...

  5. Node.js ORM 框架 sequelize 实践

    最近在做团队的一个内部系统,这次使用的nodejs web框架是团队统一的hapi.js,而数据库依然是mysql,ORM 框架选用有着6000+ stars 的 sequelize.js,hapi- ...

  6. [转]Node.JS使用Sequelize操作MySQL

    Sequelize官方文档  https://sequelize.readthedocs.io/en/latest/ 本文转自:https://www.jianshu.com/p/797e10fe23 ...

  7. node.js使用Sequelize 操作mysql

    Sequelize就是Node上的ORM框架 ,相当于java端的Hibernate 是一个基于 promise 的 Node.js ORM, 目前支持 Postgres, MySQL, SQLite ...

  8. node.js & ORM & ODM

    node.js & ORM & ODM ODM & NoSQL Object Data Modeling 对象数据模型 Object Document Mapping 对象文档 ...

  9. Node.js中的ORM

    ORM2是一款基于Node.js实现的ORM框架,名字相当的霸气,算是同类框架中非常出色的一款,具体介绍请猛击:https://github.com/dresende/node-orm2 刚接触Nod ...

  10. Node.js: What is the best "full stack web framework" (with scaffolding, MVC, ORM, etc.) based on Node.js / server-side JavaScript? - Quora

    Node.js: What is the best "full stack web framework" (with scaffolding, MVC, ORM, etc.) ba ...

随机推荐

  1. ITIL现有版本之间的区别

    时代在变化,运维管理理论也在不断演进升级,不断学习是运维人的良好品质:虽然人有的时候会懈怠,理论学习的道路也较单调乏味,但终究还是要跟上时代的步调才能适应新的变化

  2. 源码研习 — TVM中的IR设计与技术实现

    一.关键问题 TVM中的 IR 是什么,架构设计上分几层? 解答:TVM的整体结构图如下: 概念上,分为两层:上层为面向前端组网的Relay IR, 下层为面向LLVM的底层 IR. 但从设计实现上, ...

  3. Swift中的Tuple类型

    Swift中的Tuple类型可以包含任何值,并且这些值的类型可以互相不一样.Tuple本身比较简单,需要记得也就是访问Tuple的方式. 使用变量名访问 let http404Error = (404 ...

  4. java后台@RequestBody和@RequestParam

    RequestBody 接收的是请求体里面(body)的数据 RequestParam接收的是key-value里面的参数,所以它会被切割进行处理从而可以是普通元素.数组.集合.对象等接收 get-- ...

  5. Splashtop 扩展了所有 Android 8.0 以上设备的远程控制功能

    好消息:Splashtop远程访问和远程支持软件现在支持100多个品牌的 Android 设备. 2020年9月15日,远程访问和远程支持解决方案的全球领导者 Splashtop Inc. 宣布:所有 ...

  6. DashVector + DashScope升级多模态检索

    本教程在前述教程(DashVector + ModelScope玩转多模态检索)的基础之上,基于DashScope上新推出的ONE-PEACE通用多模态表征模型结合向量检索服务DashVector来对 ...

  7. 带你彻底搞懂递归时间复杂度的Master公式

    1. 什么是Master公式 1.1 Master公式的定义 Master公式,又称为Master定理或主定理,是分析递归算法时间复杂度的一种重要工具,尤其适用于具有分治结构的递归算法. \[T(n) ...

  8. pgsql安装与主从配置搭建

    一:安装环境 查看一下安装环境:cat /etc/centos-release CentOS Linux release 7.7.1908 (Core) 二:软件下载 https://www.post ...

  9. 【asp.net】滑块验证码(分享一个从github上下载的源码)

    思路: 1. 准备好10张或20张不同规格的图片,按规格分类到不同文件夹,每个文件夹的图片从1开始顺序递增命名,为了随机选择图片.   2.前端提交规格比如200*300,根据规格选择原图,并初始化 ...

  10. Redis高可用二( 哨兵sentinel)

    Redis高可用二( 哨兵sentinel) 1.主从配置 2.配置哨兵 sentinel.conf # Example sentinel.conf bind 0.0.0.0 protected-mo ...