在Express中用Sequelize设置PostgreSQL(附代码)

127 阅读11分钟

最终,每个以Express.js作为网络应用程序运行的Node.js项目都需要一个数据库。由于大多数服务器应用程序都是无状态的,为了用多个服务器实例进行横向扩展,如果没有另一个第三方(如数据库),就没有办法持久化数据。这就是为什么开发一个带有样本数据的初始应用程序是没问题的,在这里可以在没有数据库的情况下读写数据,但在某个时候你想引入一个数据库来管理数据。该数据库将保持跨服务器的数据持久性,或者即使你的一个服务器不运行。

下面的章节将告诉你如何用Sequelize作为ORM将你的Express应用程序连接到PostgreSQL数据库。如果你还没有在你的机器上安装PostgreSQL,请到这个关于如何为你的机器安装PostgreSQL的指南。它带有一个MacOS和一个Windows的安装指南。之后,请回到本指南的下一节,了解更多关于在Express中使用PostgreSQL的信息。

在Express中安装带有Sequelize的PostgreSQL

为了将PostgreSQL连接到你的Express应用程序,我们将使用一个ORM来将信息从数据库转换到一个没有SQL语句的JavaScript应用程序。ORM是Object Related Mapping的缩写,是程序员用来在不兼容的类型之间转换数据的一种技术。更具体地说,ORM模拟了实际的数据库,所以开发者可以在编程语言(如JavaScript)中操作,而不使用数据库查询语言(如SQL)来与数据库进行交互。缺点是额外的代码抽象,这就是为什么有些开发者主张反对ORM,但对于许多没有复杂数据库查询的JavaScript应用程序来说,这不应该是一个问题。

对于这个应用程序,我们将使用Sequelize作为ORM,因为它支持多种方言,其中之一就是PostgreSQL。Sequelize提供了一个舒适的API,从设置到执行都可以与PostgreSQL数据库合作,但如果你想扩展你的工具带,有许多ORM(如TypeORM,Objection.js)可供Node.js应用程序选择。

在你可以在你的Node.js应用程序中实现数据库使用之前,在命令行中为你的Node.js应用程序安装sequelize和pg,这是Node.js的postgres客户端。

npm install pg sequelize --save

在你将这两个库作为node包安装后,我们将用模型和模式来规划和实现我们的数据库实体。

数据库模型、模式和实体

下面的案例为你的应用程序实现了一个有两个数据库实体的数据库。用户和消息。通常一个数据库实体也被称为数据库模式或数据库模型。你可以用以下方式来区分它们:

  • 数据库模式。数据库模式接近于实现细节,它告诉数据库(和开发者)一个实体(例如用户实体)在数据库表中的样子,而实体的每个实例都由表行表示。例如,模式定义了一个实体的字段(如用户名)和关系(如一个用户有消息)。每个字段在数据库中被表示为一个列。基本上,模式是一个实体的蓝图。

  • 数据库模型。数据库模型是对模式的一种更抽象的看法。它为开发者提供了一个概念框架,说明有哪些模型可用,以及如何使用模型作为接口,将应用程序连接到数据库,与实体进行交互。通常,模型是用ORM来实现的。

  • 数据库实体。一个数据库实体是数据库中一个存储项目的实际实例,它是用数据库模式创建的。每个数据库实体在数据库表中使用一行,而实体的每个字段由一个列来定义。与另一个实体的关系通常用另一个实体的标识符来描述,最终也会成为数据库中的字段。

在深入研究你的应用程序的代码之前,绘制实体之间的关系以及如何处理它们之间必须传递的数据,总是一个好主意。UML(统一建模语言)图是一种直接表达实体间关系的方式,可以在你打字的时候快速引用。这对于为一个应用程序打基础的人以及任何想在数据库模式中增加信息的人来说都很有用。一个UML图可以这样显示。

uml diagram

用户和消息实体都有字段,定义了它们在结构中的身份和它们之间的关系。让我们回到我们的Express应用程序。通常,在你的Node.js应用程序中有一个叫做src/models/的文件夹,它包含了数据库中每个模型的文件(例如src/models/user.jssrc/models/message.js)。每个模型都是以定义字段和关系的模式来实现的。通常还有一个文件(例如src/models/index.js)将所有模型结合起来,并将所有模型作为数据库接口导出到 Express 应用程序中。我们可以从src/models/[modelname].js文件中的两个模型开始,为了简单起见,可以像下面这样表达,不涵盖 UML 图中的所有字段。首先,是src/models/user.js文件中的用户模型。

const getUserModel = (sequelize, { DataTypes }) => {
  const User = sequelize.define('user', {
    username: {
      type: DataTypes.STRING,
      unique: true,
      allowNull: false,
      validate: {
        notEmpty: true,
      },
    },
  });

  return User;
};

export default getUserModel;

你可以看到,用户有一个用户名字段,它被表示为字符串类型。此外,我们还为我们的用户实体增加了一些验证。首先,我们不希望在数据库中出现重复的用户名,因此我们为该字段添加了唯一属性。其次,我们想让用户名字符串成为必填项,这样就不会出现没有用户名的用户。每个用户将自动带有一个createdAt 和一个updatedAt 字段。

接下来,我们可能想把用户和消息联系起来。由于一个用户可以有很多消息,我们使用1到N的关联。

const getUserModel = (sequelize, { DataTypes }) => {
  const User = sequelize.define('user', {
    username: {
      type: DataTypes.STRING,
      unique: true,
      allowNull: false,
      validate: {
        notEmpty: true,
      },
    },
  });

  User.associate = (models) => {
    User.hasMany(models.Message, { onDelete: 'CASCADE' });
  };

  return User;
};

export default getUserModel;

我们还可以在我们的模型上实现额外的方法。让我们假设我们的用户实体在未来最终会有一个电子邮件字段。那么我们可以添加一个方法,通过他们的一个抽象的 "login "词,也就是最后的用户名或电子邮件,在数据库中找到一个用户。当用户能够通过用户名或电子邮件地址登录到你的应用程序时,这很有帮助。你可以把它作为你的模型的方法来实现。之后,这个方法会在你选择的ORM中的所有其他内置方法旁边可用。

const getUserModel = (sequelize, { DataTypes }) => {
  const User = sequelize.define('user', {
    username: {
      type: DataTypes.STRING,
      unique: true,
      allowNull: false,
      validate: {
        notEmpty: true,
      },
    },
  });

  User.associate = (models) => {
    User.hasMany(models.Message);
  };

  User.findByLogin = async (login) => {
    let user = await User.findOne({
      where: { username: login },
    });

    if (!user) {
      user = await User.findOne({
        where: { email: login },
      });
    }

    return user;
  };

  return User;
};

export default getUserModel;

消息模型看起来很相似,尽管我们没有给它添加任何自定义方法,而且字段也很简单,只有一个文本字段和另一个与用户相关的消息。

const getMessageModel = (sequelize, { DataTypes }) => {
  const Message = sequelize.define('message', {
    text: {
      type: DataTypes.STRING,
      allowNull: false,
      validate: {
        notEmpty: true,
      },
    },
  });

  Message.associate = (models) => {
    Message.belongsTo(models.User);
  };

  return Message;
};

export default getMessageModel;

现在,如果一个用户被删除,我们可能想对所有与该用户有关的消息进行所谓的级联删除。这就是为什么你可以用一个CASCADE标志来扩展模式。在这种情况下,我们把这个标志添加到我们的用户模式中,以便在该用户被删除时删除它的所有消息。

const getUserModel = (sequelize, { DataTypes }) => {
  const User = sequelize.define('user', {
    username: {
      type: DataTypes.STRING,
      unique: true,
      allowNull: false,
      validate: {
        notEmpty: true,
      },
    },
  });

  User.associate = (models) => {
    User.hasMany(models.Message, { onDelete: 'CASCADE' });
  };

  User.findByLogin = async (login) => {
    let user = await User.findOne({
      where: { username: login },
    });

    if (!user) {
      user = await User.findOne({
        where: { email: login },
      });
    }

    return user;
  };

  return User;
};

export default getUserModel;

Sequelize用于定义具有其内容的模式(由DataTypes 和可选配置组成)。此外,还可以添加额外的方法来塑造数据库接口,关联属性用于创建模型之间的关系。一个用户可以有多个消息,但一个消息只属于一个用户。你可以在Sequelize文档中更深入地了解这些概念。接下来,在你的src/models/index.js文件中,导入并组合这些模型,并使用 Sequelize API 解决它们的关联。

import Sequelize from 'sequelize';

import getUserModel from './user';
import getMessageModel from './message';

const sequelize = new Sequelize(
  process.env.DATABASE,
  process.env.DATABASE_USER,
  process.env.DATABASE_PASSWORD,
  {
    dialect: 'postgres',
  },
);

const models = {
  User: getUserModel(sequelize, Sequelize),
  Message: getMessageModel(sequelize, Sequelize),
};

Object.keys(models).forEach((key) => {
  if ('associate' in models[key]) {
    models[key].associate(models);
  }
});

export { sequelize };

export default models;

在文件的顶部,你通过向构造函数传递强制性参数(数据库名称、数据库超级用户、数据库超级用户的密码和附加配置)来创建一个Sequelize实例。例如,你需要告诉Sequelize你的数据库的方言,它是postgres而不是mysql或sqlite。在我们的案例中,我们使用的是环境变量,但你也可以在源代码中以字符串的形式传递这些参数。例如,环境变量在一个*.env*文件中可以是下面这样的。

DATABASE=mydatabase
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres

注意:如果你的应用程序还没有一个超级用户或专用数据库,请前往PostgreSQL设置指南来创建它们。你只需要创建一次超级用户,但你的每个应用程序都应该有自己的数据库。

最后,在你的Express应用程序中使用创建的Sequelize实例。它以异步方式连接到数据库,一旦完成,你就可以启动你的Express应用程序。

import express from 'express';
...

import models, { sequelize } from './models';

const app = express();

...

sequelize.sync().then(() => {
  app.listen(process.env.PORT, () => {
    console.log(`Example app listening on port ${process.env.PORT}!`);
  });
});

如果你想在每次启动Express服务器时重新初始化你的数据库,你可以给你的同步方法添加一个条件。

...
...

const eraseDatabaseOnSync = true;

sequelize.sync({ force: eraseDatabaseOnSync }).then(async () => {
  app.listen(process.env.PORT, () =>
    console.log(`Example app listening on port ${process.env.PORT}!`),
  );
});

这就是为你的Express应用程序定义数据库模型,并在你启动你的应用程序时将所有东西连接到数据库。一旦你再次启动你的应用程序,命令行的结果将显示你的数据库中的表是如何创建的。

如何为PostgreSQL数据库播种?

最后但并非最不重要的是,你可能想用初始数据来启动你的PostgreSQL数据库。否则,在每次应用程序启动时清除数据库(例如,eraseDatabaseOnSync)时,你将总是从一张白纸开始。

在我们的案例中,我们的数据库里有用户和消息实体。每条消息都与一个用户相关联。现在,每次你启动你的应用程序时,你的数据库都会连接到你的物理数据库。这就是你决定在你的源代码中用一个布尔标志清除你所有的数据。同时这也可能是为你的数据库播种初始数据的地方。

...
...

const eraseDatabaseOnSync = true;

sequelize.sync({ force: eraseDatabaseOnSync }).then(async () => {
  if (eraseDatabaseOnSync) {
    createUsersWithMessages();
  }

  app.listen(process.env.PORT, () =>
    console.log(`Example app listening on port ${process.env.PORT}!`),
  );
});

const createUsersWithMessages = async () => {
  ...
};

createUsersWithMessages() 函数将被用来为我们的数据库播种。播种是异步进行的,因为在数据库中创建数据不是一个同步的任务。让我们看看我们如何用Sequelize在PostgreSQL中创建我们的第一个用户。

...

const createUsersWithMessages = async () => {
  await models.User.create(
    {
      username: 'rwieruch',
    },
  );
};

我们的每个用户实体只有一个用户名作为属性。但是这个用户的信息呢?我们可以在一个函数中与用户一起创建它们。

...

const createUsersWithMessages = async () => {
  await models.User.create(
    {
      username: 'rwieruch',
      messages: [
        {
          text: 'Published the Road to learn React',
        },
      ],
    },
    {
      include: [models.Message],
    },
  );
};

我们可以说,我们的用户实体应该和消息实体一起创建。由于一个消息只有一个文本,我们可以将这些文本作为数组传递给用户创建。然后,每个消息实体将与一个具有用户标识符的用户相关联。让我们创建第二个用户,但这次要有两条消息。

...

const createUsersWithMessages = async () => {
  await models.User.create(
    {
      username: 'rwieruch',
      messages: [
        {
          text: 'Published the Road to learn React',
        },
      ],
    },
    {
      include: [models.Message],
    },
  );

  await models.User.create(
    {
      username: 'ddavids',
      messages: [
        {
          text: 'Happy to release ...',
        },
        {
          text: 'Published a complete ...',
        },
      ],
    },
    {
      include: [models.Message],
    },
  );
};

就这样了。在我们的案例中,我们已经使用我们的模型来创建具有关联消息的用户。这发生在应用程序启动时,我们想从一个干净的地方开始;这被称为数据库播种。然而,我们的模型的API在我们的应用程序中以同样的方式用于创建用户和消息。最后,我们已经在一个带有Express的Node.js应用程序中设置了PostgreSQL。缺少的是将数据库连接到Express,以使用户能够用API对数据库进行操作,而不是对样本数据进行操作。