跳到内容

c

迁移,多对多关系

Migrations

让我们继续扩展后端。我们想实现对允许具有管理员身份的用户将他们选择的用户置于禁用模式的支持,防止他们登录和创建新的笔记。为了实现这一点,我们需要在用户的数据库表中添加布尔字段,表明用户是否是管理员和用户是否被禁用。

我们可以像以前那样进行,即改变定义该表的模型,并依靠Sequelize将变化同步到数据库中。这是由文件models/index.js中的这几行指定的。

const Note = require('./note')
const User = require('./user')

Note.belongsTo(User)
User.hasMany(Note)

Note.sync({ alter: true })User.sync({ alter: true })
module.exports = {
  Note, User
}

然而,从长远来看,这种方法是没有意义的。让我们删除这些进行同步的行,转而使用一种更稳健的方式,即Sequelize(和许多其他库)提供的migrations

在实践中,迁移是一个单一的JavaScript文件,描述了对数据库的一些修改。一个单独的迁移文件是为每一个单一的或多个变化一次性创建的。Sequelize会记录哪些迁移已经被执行,也就是说,哪些由迁移引起的变化被同步到了数据库模式中。当创建新的迁移时,Sequelize会及时了解哪些数据库模式的变化还没有进行。通过这种方式,修改是以一种可控的方式进行的,程序代码存储在版本控制中。

首先,让我们创建一个初始化数据库的迁移。迁移的代码如下

const { DataTypes } = require('sequelize')

module.exports = {
  up: async ({ context: queryInterface }) => {
    await queryInterface.createTable('notes', {
      id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true
      },
      content: {
        type: DataTypes.TEXT,
        allowNull: false
      },
      important: {
        type: DataTypes.BOOLEAN
      },
      date: {
        type: DataTypes.DATE
      },
    })
    await queryInterface.createTable('users', {
      id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true
      },
      username: {
        type: DataTypes.STRING,
        unique: true,
        allowNull: false
      },
      name: {
        type: DataTypes.STRING,
        allowNull: false
      },
    })
    await queryInterface.addColumn('notes', 'user_id', {
      type: DataTypes.INTEGER,
      allowNull: false,
      references: { model: 'users', key: 'id' },
    })
  },
  down: async ({ context: queryInterface }) => {
    await queryInterface.dropTable('notes')
    await queryInterface.dropTable('users')
  },
}

迁移文件定义了函数updown,其中第一个函数定义了在执行迁移时应该如何修改数据库。函数down告诉你,如果有必要,如何撤销迁移。

我们的迁移包含三个操作,第一个创建一个notes表,第二个创建一个users表,第三个给notes表添加一个外键,引用笔记的创建者。模式中的变化是通过调用queryInterface对象方法来定义的。

在定义迁移时,一定要记住,与模型不同,列名和表名是以蛇形大小写的形式书写的。

await queryInterface.addColumn('notes', 'user_id', {  type: DataTypes.INTEGER,
  allowNull: false,
  references: { model: 'users', key: 'id' },
})

所以在迁移中,表和列的名字是按照它们在数据库中出现的样子写的,而模型则使用Sequelize's默认的camelCase命名规则。

将迁移代码保存在文件migrations/20211209_00_initialize_notes_and_users.js。迁移文件名在创建时应该总是按字母顺序命名,这样以前的修改总是在新的修改之前。实现这种顺序的一个好方法是在迁移文件名中以日期和序列号开始。

我们可以使用Sequelize命令行工具从命令行中运行迁移。然而,我们选择使用Umzug库从程序代码中手动执行迁移。让我们安装这个库

npm install umzug

让我们修改处理与数据库连接的util/db.js文件,如下所示。

const Sequelize = require('sequelize')
const { DATABASE_URL } = require('./config')
const { Umzug, SequelizeStorage } = require('umzug')
const sequelize = new Sequelize(DATABASE_URL, {
  dialectOptions: {
    ssl: {
      require: true,
      rejectUnauthorized: false
    }
  },
});

const runMigrations = async () => {  const migrator = new Umzug({    migrations: {      glob: 'migrations/*.js',    },    storage: new SequelizeStorage({ sequelize, tableName: 'migrations' }),    context: sequelize.getQueryInterface(),    logger: console,  })  const migrations = await migrator.up()  console.log('Migrations up to date', {    files: migrations.map((mig) => mig.name),  })}
const connectToDatabase = async () => {
  try {
    await sequelize.authenticate()
    await runMigrations()    console.log('connected to the database')
  } catch (err) {
    console.log('failed to connect to the database')
    console.log(err)
    return process.exit(1)
  }

  return null
}

module.exports = { connectToDatabase, sequelize }

执行迁移的runMigrations函数现在会在应用启动时每次打开数据库连接时执行。Sequelize会跟踪哪些迁移已经完成,所以如果没有新的迁移,运行runMigrations函数不会有任何作用。

现在让我们从一块干净的石板开始,从应用中删除所有现有的数据库表。

username => drop table notes;
username => drop table users;
username => \d
Did not find any relations.

让我们启动应用。一条关于迁移状态的信息被打印在日志上

INSERT INTO "migrations" ("name") VALUES ($1) RETURNING "name";
Migrations up to date { files: [ '20211209_00_initialize_notes_and_users.js' ] }
database connected

如果我们重新启动应用,日志中也显示迁移没有重复。

应用的数据库模式现在看起来是这样的

postgres=# \d
                 List of relations
 Schema |     Name     |   Type   |     Owner
--------+--------------+----------+----------------
 public | migrations   | table    | username
 public | notes        | table    | username
 public | notes_id_seq | sequence | username
 public | users        | table    | username
 public | users_id_seq | sequence | username

So Sequelize has created a migrations table that allows it to keep track of the migrations that have been performed. The contents of the table look as follows:

postgres=# select * from migrations;
                   name
-------------------------------------------
 20211209_00_initialize_notes_and_users.js

让我们在数据库中创建几个用户,以及一组笔记,之后我们就可以扩展应用了。

该应用的当前代码全部在GitHub,分支part13-6

Admin user and user disabling

所以我们想在users表中添加两个布尔字段

  • admin告诉你该用户是否是一个管理员

  • disabled告诉你该用户是否被禁止行动

让我们在文件migrations/20211209/01_admin/and/disabled/to/users.js中创建修改数据库的迁移。

const { DataTypes } = require('sequelize')

module.exports = {
  up: async ({ context: queryInterface }) => {
    await queryInterface.addColumn('users', 'admin', {
      type: DataTypes.BOOLEAN,
      default: false
    })
    await queryInterface.addColumn('users', 'disabled', {
      type: DataTypes.BOOLEAN,
      default: false
    })
  },
  down: async ({ context: queryInterface }) => {
    await queryInterface.removeColumn('users', 'admin')
    await queryInterface.removeColumn('users', 'disabled')
  },
}

users表对应的模型做相应的修改。

User.init({
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  username: {
    type: DataTypes.STRING,
    unique: true,
    allowNull: false
  },
  name: {
    type: DataTypes.STRING,
    allowNull: false
  },
  admin: {    type: DataTypes.BOOLEAN,    defaultValue: false  },  disabled: {    type: DataTypes.BOOLEAN,    defaultValue: false  },}, {
  sequelize,
  underscored: true,
  timestamps: false,
  modelName: 'user'
})

当代码重新启动时进行新的迁移,模式会按要求进行改变。

username-> \d users
                                     Table "public.users"
  Column  |          Type          | Collation | Nullable |              Default
----------+------------------------+-----------+----------+-----------------------------------
 id       | integer                |           | not null | nextval('users_id_seq'::regclass)
 username | character varying(255) |           | not null |
 name     | character varying(255) |           | not null |
 admin    | boolean                |           |          |
 disabled | boolean                |           |          |
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)
    "users_username_key" UNIQUE CONSTRAINT, btree (username)
Referenced by:
    TABLE "notes" CONSTRAINT "notes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)

Now let's expand the controllers as follows. We prevent logging in if the user field disabled is set to true:

loginRouter.post('/', async (request, response) => {
  const body = request.body

  const user = await User.findOne({
    where: {
      username: body.username
    }
  })

  const passwordCorrect = body.password === 'secret'

  if (!(user && passwordCorrect)) {
    return response.status(401).json({
      error: 'invalid username or password'
    })
  }

  if (user.disabled) {    return response.status(401).json({      error: 'account disabled, please contact admin'    })  }
  const userForToken = {
    username: user.username,
    id: user.id,
  }

  const token = jwt.sign(userForToken, process.env.SECRET)

  response
    .status(200)
    .send({ token, username: user.username, name: user.name })
})

Let's disable the user jakousa using his ID:

username => update users set disabled=true where id=3;
UPDATE 1
username => update users set admin=true where id=1;
UPDATE 1
username => select * from users;
 id | username |       name       | admin | disabled
----+----------+------------------+-------+----------
  2 | lynx     | Kalle Ilves      |       |
  3 | jakousa  | Jami Kousa       | f     | t
  1 | mluukkai | Matti Luukkainen | t     |

并确保登录不再可能发生

fullstack content

让我们创建一个路由,允许管理员改变一个用户的账户状态。

const isAdmin = async (req, res, next) => {
  const user = await User.findByPk(req.decodedToken.id)
  if (!user.admin) {
    return res.status(401).json({ error: 'operation not allowed' })
  }
  next()
}

router.put('/:username', tokenExtractor, isAdmin, async (req, res) => {
  const user = await User.findOne({
    where: {
      username: req.params.username
    }
  })

  if (user) {
    user.disabled = req.body.disabled
    await user.save()
    res.json(user)
  } else {
    res.status(404).end()
  }
})

使用了两个中间件,第一个叫做tokenExtractor的中间件与创建笔记的路由所使用的相同,即它将解码的令牌放在请求对象的decodedToken域中。第二个中间件isAdmin检查用户是否是管理员,如果不是,请求状态被设置为401,并返回一个适当的错误信息。

请注意两个中间件是如何被链入路由的,这两个中间件都在实际的路由处理程序之前执行。有可能将任意数量的中间件链接到一个请求。

中间件tokenExtractor现在被移到util/middleware.js,因为它从多个位置被使用。

const jwt = require('jsonwebtoken')
const { SECRET } = require('./config.js')

const tokenExtractor = (req, res, next) => {
  const authorization = req.get('authorization')
  if (authorization && authorization.toLowerCase().startsWith('bearer ')) {
    try {
      req.decodedToken = jwt.verify(authorization.substring(7), SECRET)
    } catch{
      return res.status(401).json({ error: 'token invalid' })
    }
  } else {
    return res.status(401).json({ error: 'token missing' })
  }
  next()
}

module.exports = { tokenExtractor }

管理员现在可以通过向/api/users/jakousa发出PUT请求来重新启用用户jakousa,该请求带有以下数据。

{
    "disabled": false
}

正如在第四章节的结尾所指出的,我们在这里实现禁用用户的方式是有问题的。用户是否被禁用只在登录时检查,如果用户在被禁用时有一个令牌,那么用户可以继续使用同一个令牌,因为没有为令牌设置寿命,而且在创建笔记时没有检查用户的禁用状态。

在我们继续之前,让我们为应用做一个npm脚本,它允许我们撤销之前的迁移。毕竟,在开发迁移时,并不是第一次就能顺利进行的。

让我们修改文件util/db.js如下。

const Sequelize = require('sequelize')
const { DATABASE_URL } = require('./config')
const { Umzug, SequelizeStorage } = require('umzug')

const sequelize = new Sequelize(DATABASE_URL, {
  dialectOptions: {
    ssl: {
      require: true,
      rejectUnauthorized: false
    }
  },
});

const connectToDatabase = async () => {
  try {
    await sequelize.authenticate()
    await runMigrations()
    console.log('connected to the database')
  } catch (err) {
    console.log('failed to connect to the database')
    return process.exit(1)
  }

  return null
}

const migrationConf = {  migrations: {    glob: 'migrations/*.js',  },  storage: new SequelizeStorage({ sequelize, tableName: 'migrations' }),  context: sequelize.getQueryInterface(),  logger: console,}const runMigrations = async () => {  const migrator = new Umzug(migrationConf)  const migrations = await migrator.up()  console.log('Migrations up to date', {    files: migrations.map((mig) => mig.name),  })}const rollbackMigration = async () => {  await sequelize.authenticate()  const migrator = new Umzug(migrationConf)  await migrator.down()}
module.exports = { connectToDatabase, sequelize, rollbackMigration }

让我们创建一个文件util/rollback.js,它将允许npm脚本执行指定的迁移回滚功能。

const { rollbackMigration } = require('./db')

rollbackMigration()

和脚本本身。

{
  "scripts": {
    "dev": "nodemon index.js",
    "migration:down": "node util/rollback.js"  },
}

所以我们现在可以通过在命令行中运行npm run migration:down来撤销之前的迁移。

迁移目前是在程序启动时自动执行的。在程序的开发阶段,有时禁用迁移的自动执行,从命令行手动进行迁移可能更合适。

目前该程序的代码全部在GitHub,分支part13-7

Many-to-many relationships

我们将继续扩展应用,使每个用户可以被加入一个或多个团队

由于任意数量的用户可以加入一个团队,而一个用户可以加入任意数量的团队,我们正在处理一个多对多的关系,传统上这是在关系型数据库中使用连接表来实现。

现在让我们为团队表以及连接表创建所需的代码。迁移过程如下。

const { DataTypes } = require('sequelize')

module.exports = {
  up: async ({ context: queryInterface }) => {
    await queryInterface.createTable('teams', {
      id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true
      },
      name: {
        type: DataTypes.TEXT,
        allowNull: false,
        unique: true
      },
    })
    await queryInterface.createTable('memberships', {
      id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true
      },
      user_id: {
        type: DataTypes.INTEGER,
        allowNull: false,
        references: { model: 'users', key: 'id' },
      },
      team_id: {
        type: DataTypes.INTEGER,
        allowNull: false,
        references: { model: 'teams', key: 'id' },
      },
    })
  },
  down: async ({ context: queryInterface }) => {
    await queryInterface.dropTable('teams')
    await queryInterface.dropTable('memberships')
  },
}

模型包含的代码几乎与迁移相同。团队模型在models/team.js

const { Model, DataTypes } = require('sequelize')

const { sequelize } = require('../util/db')

class Team extends Model {}

Team.init({
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  name: {
    type: DataTypes.TEXT,
    allowNull: false,
    unique: true
  },
}, {
  sequelize,
  underscored: true,
  timestamps: false,
  modelName: 'team'
})

module.exports = Team

连接表的模型在models/membership.js

const { Model, DataTypes } = require('sequelize')

const { sequelize } = require('../util/db')

class Membership extends Model {}

Membership.init({
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  user_id: {
    type: DataTypes.INTEGER,
    allowNull: false,
    references: { model: 'users', key: 'id' },
  },
  team_id: {
    type: DataTypes.INTEGER,
    allowNull: false,
    references: { model: 'teams', key: 'id' },
  },
}, {
  sequelize,
  underscored: true,
  timestamps: false,
  modelName: 'membership'
})

module.exports = Membership

所以我们给连接表起了一个能很好描述它的名字,membership。连接表并不总是有一个相关的名字,在这种情况下,连接表的名字可以是被连接的表的名字的组合,例如,user/_teams可以适合我们的情况。

我们对models/index.js文件做了一个小小的补充,使用belongsToMany方法在代码层连接团队和用户。

const Note = require('./note')
const User = require('./user')
const Team = require('./team')const Membership = require('./membership')
Note.belongsTo(User)
User.hasMany(Note)

User.belongsToMany(Team, { through: Membership })Team.belongsToMany(User, { through: Membership })
module.exports = {
  Note, User, Team, Membership}

注意在定义外键字段时,连接表和模型的迁移是不同的。在迁移过程中,字段是以蛇形的形式定义的。

await queryInterface.createTable('memberships', {
  // ...
  user_id: {
    type: DataTypes.INTEGER,
    allowNull: false,
    references: { model: 'users', key: 'id' },
  },
  team_id: {
    type: DataTypes.INTEGER,
    allowNull: false,
    references: { model: 'teams', key: 'id' },
  }
})

在模型中,同样的字段是以骆驼的形式定义的。

Membership.init({
  // ...
  userId: {
    type: DataTypes.INTEGER,
    allowNull: false,
    references: { model: 'users', key: 'id' },
  },
  teamId: {
    type: DataTypes.INTEGER,
    allowNull: false,
    references: { model: 'teams', key: 'id' },
  },
  // ...
})

现在让我们从控制台创建几个团队,以及一些会员资格。

insert into teams (name) values ('toska');
insert into teams (name) values ('mosa climbers');
insert into memberships (user_id, team_id) values (1, 1);
insert into memberships (user_id, team_id) values (1, 2);
insert into memberships (user_id, team_id) values (2, 1);
insert into memberships (user_id, team_id) values (3, 2);

关于用户团队的信息将被添加到检索所有用户的路径中

router.get('/', async (req, res) => {
  const users = await User.findAll({
    include: [
      {
        model: Note,
        attributes: { exclude: ['userId'] }
      },
      {        model: Team,        attributes: ['name', 'id'],      }    ]
  })
  res.json(users)
})

最善于观察的人会注意到,打印到控制台的查询现在结合了三个表。

这个解决方案相当不错,但其中有一个美丽的缺陷。结果还带有连接表相应行的属性,尽管我们并不希望这样。

fullstack content

通过仔细阅读文档,你可以找到一个解决方案

router.get('/', async (req, res) => {
  const users = await User.findAll({
    include: [
      {
        model: Note,
        attributes: { exclude: ['userId'] }
      },
      {
        model: Team,
        attributes: ['name', 'id'],
        through: {          attributes: []        }      }
    ]
  })
  res.json(users)
})

该应用的当前代码全部在GitHub,分支part13-8

Note on the properties of Sequelize model objects

我们模型的规范由以下几行显示。

User.hasMany(Note)
Note.belongsTo(User)

User.belongsToMany(Team, { through: Membership })
Team.belongsToMany(User, { through: Membership })

这些允许Sequelize进行查询,例如,检索用户的所有笔记,或一个团队的所有成员。

由于这些定义,我们也可以直接访问,例如,代码中用户的笔记。在下面的代码中,我们将搜索一个id为1的用户,并打印与该用户相关的笔记。

const user = await User.findByPk(1, {
  include: {
    model: Note
  }
})

user.notes.forEach(note => {
  console.log(note.content)
})

因此,User.hasMany(Note)定义给user对象附加了一个notes属性,它可以访问该用户的笔记。User. belongsToMany(Team, { through: Membership }))定义同样将一个teams属性附加到user对象,这也可以在代码中使用。

const user = await User.findByPk(1, {
  include: {
    model: team
  }
})

user.teams.forEach(team => {
  console.log(team.name)
})

假设我们想从单个用户的路由中返回一个JSON对象,包含用户的名字、用户名和创建的笔记数量。我们可以尝试以下方法。

router.get('/:id', async (req, res) => {
  const user = await User.findByPk(req.params.id, {
    include: {
        model: Note
      }
    }
  )

  if (user) {
    user.note_count = user.notes.length    delete user.notes    res.json(user)

  } else {
    res.status(404).end()
  }
})

所以,我们尝试在Sequelize返回的对象上添加noteCount字段,并删除其中的notes字段。然而,这种方法并不奏效,因为Sequelize返回的对象并不是正常的对象,在那里添加新的字段会按照我们的意图工作。

一个更好的解决方案是根据从数据库中获取的数据创建一个全新的对象。

router.get('/:id', async (req, res) => {
  const user = await User.findByPk(req.params.id, {
    include: {
        model: Note
      }
    }
  )

  if (user) {
    res.json({
      username: user.username,      name: user.name,      note_count: user.notes.length    })

  } else {
    res.status(404).end()
  }
})

Revisiting many-to-many relationships

让我们在应用中建立另一个多对多的关系。每个笔记都通过一个外键与创建它的用户相关联。现在决定,应用还支持笔记可以与其他用户相关联,并且一个用户可以与其他用户创建的任意数量的笔记相关联。我们的想法是,这些笔记是用户为自己标记的

让我们为这种情况制作一个连接表user_notes。迁移是直截了当的。

const { DataTypes } = require('sequelize')

module.exports = {
  up: async ({ context: queryInterface }) => {
    await queryInterface.createTable('user_notes', {
      id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true
      },
      user_id: {
        type: DataTypes.INTEGER,
        allowNull: false,
        references: { model: 'users', key: 'id' },
      },
      note_id: {
        type: DataTypes.INTEGER,
        allowNull: false,
        references: { model: 'notes', key: 'id' },
      },
    })
  },
  down: async ({ context: queryInterface }) => {
    await queryInterface.dropTable('user_notes')
  },
}

另外,这个模型也没有什么特别之处。

const { Model, DataTypes } = require('sequelize')

const { sequelize } = require('../util/db')

class UserNotes extends Model {}

UserNotes.init({
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  userId: {
    type: DataTypes.INTEGER,
    allowNull: false,
    references: { model: 'users', key: 'id' },
  },
  noteId: {
    type: DataTypes.INTEGER,
    allowNull: false,
    references: { model: 'notes', key: 'id' },
  },
}, {
  sequelize,
  underscored: true,
  timestamps: false,
  modelName: 'user_notes'
})

module.exports = UserNotes

文件models/index.js,另一方面,与我们之前看到的有一点变化。

const Note = require('./note')
const User = require('./user')
const Team = require('./team')
const Membership = require('./membership')
const UserNotes = require('./user_notes')
Note.belongsTo(User)
User.hasMany(Note)

User.belongsToMany(Team, { through: Membership })
Team.belongsToMany(User, { through: Membership })

User.belongsToMany(Note, { through: UserNotes, as: 'marked_notes' })Note.belongsToMany(User, { through: UserNotes, as: 'users_marked' })
module.exports = {
  Note, User, Team, Membership, UserNotes
}

再次使用了belongsToMany,现在它通过连接表对应的UserNotes模型将用户与笔记联系起来。然而,这一次我们为使用关键字as形成的属性给出了一个别名,默认名称(用户的笔记)将与它之前的含义重叠,即由用户创建的笔记。

我们为单个用户扩展了路线,以返回用户的团队、他们自己的笔记,以及由用户标记的其他笔记。

router.get('/:id', async (req, res) => {
  const user = await User.findByPk(req.params.id, {
    attributes: { exclude: [''] } ,
    include:[{
        model: Note,
        attributes: { exclude: ['userId'] }
      },
      {
        model: Note,
        as: 'marked_notes',
        attributes: { exclude: ['userId']},
        through: {
          attributes: []
        }
      },
      {
        model: Team,
        attributes: ['name', 'id'],
        through: {
          attributes: []
        }
      },
    ]
  })

  if (user) {
    res.json(user)
  } else {
    res.status(404).end()
  }
})

在include的上下文中,我们现在必须使用别名marked_notes,我们刚刚用as属性定义了它。

为了测试这个功能,让我们在数据库中创建一些测试数据。

insert into user_notes (user_id, note_id) values (1, 4);
insert into user_notes (user_id, note_id) values (1, 5);

最终的结果是功能性的。

fullstack content

如果我们想在用户标记的笔记中也包括关于笔记作者的信息呢?这可以通过在标记的笔记中添加一个include来实现。

router.get('/:id', async (req, res) => {
  const user = await User.findByPk(req.params.id, {
    attributes: { exclude: [''] } ,
    include:[{
        model: Note,
        attributes: { exclude: ['userId'] }
      },
      {
        model: Note,
        as: 'marked_notes',
        attributes: { exclude: ['userId']},
        through: {
          attributes: []
        },
        include: {          model: User,          attributes: ['name']        }      },
      {
        model: Team,
        attributes: ['name', 'id'],
        through: {
          attributes: []
        }
      },
    ]
  })

  if (user) {
    res.json(user)
  } else {
    res.status(404).end()
  }
})

最后的结果是如愿以偿。

fullstack content

该应用的当前代码全部在GitHub,分支part13-9

Concluding remarks

我们的应用的状态开始至少可以接受。然而,在本节结束前,让我们再看几点。

Eager vs lazy fetch

当我们使用include属性进行查询时。

User.findOne({
  include: {
    model: note
  }
})

所谓的急切获取会发生,也就是说,所有通过连接查询连接到用户的表的行,在这个例子中是用户做的笔记,都会同时从数据库获取。这通常是我们想要的,但也有一些情况,你想做一个所谓的lazy fetch,例如,只有在需要时才搜索用户相关的团队。

现在让我们修改单个用户的路由,以便它只在请求中设置查询参数teams时才获取用户的团队。

router.get('/:id', async (req, res) => {
  const user = await User.findByPk(req.params.id, {
    attributes: { exclude: [''] } ,
    include:[{
        model: note,
        attributes: { exclude: ['userId'] }
      },
      {
        model: Note,
        as: 'marked_notes',
        attributes: { exclude: ['userId']},
        through: {
          attributes: []
        },
        include: {
          model: user,
          attributes: ['name']
        }
      },
    ]
  })

  if (!user) {
    return res.status(404).end()
  }

  let teams = undefined  if (req.query.teams) {    teams = await user.getTeams({      attributes: ['name'],      joinTableAttributes: []    })  }  res.json({ ...user.toJSON(), teams })})

所以现在,User.findByPk查询不会检索团队,但如果有必要,它们会被user方法getTeams检索,该方法是由Sequelize为模型对象自动生成。类似的get-和其他一些有用的方法是自动生成的,当在Sequelize级别定义表的关联时。

Features of models

有些情况下,默认情况下,我们不希望处理某个特定表的所有行。其中一种情况是,我们通常不想在我们的应用中显示已被禁用的用户。在这种情况下,我们可以像这样为模型定义默认的scopes

class User extends Model {}

User.init({
  // field definition
}, {
  sequelize,
  underscored: true,
  timestamps: false,
  modelName: 'user',
  defaultScope: {    where: {      disabled: false    }  },  scopes: {    admin: {      where: {        admin: true      }    },    disabled: {      where: {        disabled: true      }    }  }})

module.exports = User

现在由函数调用User.findAll()引起的查询有以下WHERE条件。

WHERE "user". "disabled" = false;

对于模型,也可以定义其他作用域。

User.init({
  // field definition
}, {
  sequelize,
  underscored: true,
  timestamps: false,
  modelName: 'user',
  defaultScope: {
    where: {
      disabled: false
    }
  },
  scopes: {    admin: {      where: {        admin: true      }    },    disabled: {      where: {        disabled: true      }    },    name(value) {      return {        where: {          name: {            [Op.iLike]: value          }        }      }    },  }})

作用域的使用方法如下。

// all admins
const adminUsers = await User.scope('admin').findAll()

// all inactive users
const disabledUsers = await User.scope('disabled').findAll()

// users with the string jami in their name
const jamiUsers = User.scope({ method: ['name', '%jami%'] }).findAll()

也可以连锁作用域。

// admins with the string jami in their name
const jamiUsers = User.scope('admin', { method: ['name', '%jami%'] }).findAll()

由于Sequelize模型是正常的JavaScript类,所以有可能向它们添加新的方法。

这里有两个例子。

const { Model, DataTypes, Op } = require('sequelize')
const Note = require('./note')
const { sequelize } = require('../util/db')

class User extends Model {
  async number_of_notes() {    return (await this.getNotes()).length  }  static async with_notes(limit){    return await User.findAll({      attributes: {        include: [[ sequelize.fn("COUNT", sequelize.col("notes.id")), "note_count" ]]      },      include: [        {          model: Note,          attributes: []        },      ],      group: ['user.id'],      having: sequelize.literal(`COUNT(notes.id) > ${limit}`)    })  }}

User.init({
  // ...
})

module.exports = User

第一个方法numberOfNotes是一个实例方法,意味着它可以在模型的实例中被调用。

const jami = await User.findOne({ name: 'Jami Kousa'})
const cnt = await jami.number_of_notes()
console.log(`Jami has created ${cnt} notes`)

在实例方法中,关键词this因此指的是实例本身。

async number_of_notes() {
  return (await this.getNotes()).length
}

第二个方法是返回那些至少有X的用户,这个数字是由参数指定的,笔记的数量是一个类方法,也就是说,它是直接在模型上调用的。

const users = await User.with_notes(2)
console.log(JSON.stringify(users, null, 2))
users.forEach(u => {
  console.log(u.name)
})

Repeatability of models and migrations

我们注意到,模型和迁移的代码是非常重复的。例如,团队的模型

class Team extends Model {}

Team.init({
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  name: {
    type: DataTypes.TEXT,
    allowNull: false,
    unique: true
  },
}, {
  sequelize,
  underscored: true,
  timestamps: false,
  modelName: 'team'
})

module.exports = Team

和迁移包含了很多相同的代码

const { DataTypes } = require('sequelize')

module.exports = {
  up: async ({ context: queryInterface }) => {
    await queryInterface.createTable('teams', {
      id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true
      },
      name: {
        type: DataTypes.TEXT,
        allowNull: false,
        unique: true
      },
    })
  },
  down: async ({ context: queryInterface }) => {
    await queryInterface.dropTable('teams')
  },
}

难道我们不能优化代码,比如说,模型导出迁移所需的共享部分?

然而,问题是,模型的定义可能会随着时间的推移而改变,例如,name字段可能会改变或其数据类型可能会改变。迁移必须能够在任何时候从头到尾成功执行,如果迁移依赖于模型的某些内容,那么在一个月或一年后,它可能不再是真的。因此,尽管有 "复制粘贴",迁移代码应该与模型代码完全分开。

一个解决方案是使用Sequelize's 命令行工具,它可以根据命令行给出的命令来生成模型和迁移文件。例如,下面的命令将创建一个User模型,并将nameusernameadmin作为属性,以及管理创建数据库表的迁移。

npx sequelize-cli model:generate --name User --attributes name:string,username:string,admin:boolean

从命令行中,你也可以运行回滚,即撤销迁移。不幸的是,命令行文档并不完整,在这个课程中,我们决定手动完成模型和迁移。这个解决方案可能是明智的,也可能不是。