Sequelize - популярный, простой в использовании инструмент объектно-реляционного сопоставления (ORM) JavaScript, который работает с базами данных SQL. Довольно просто начать новый проект с помощью интерфейса командной строки Sequelize, но чтобы по-настоящему воспользоваться возможностями Sequelize, вам нужно определить отношения между своими моделями.

В этом пошаговом руководстве мы создадим проект Sequelize, чтобы назначать задачи конкретным пользователям. Мы воспользуемся ассоциациями для определения этой связи, а затем рассмотрим способы запроса базы данных на основе этих ассоциаций.

Начнем с установки Postgres, Sequelize и Sequelize CLI в новую папку проекта:

mkdir sequelize-associations
cd sequelize-associations
npm init -y
npm install sequelize pg
npm install --save-dev sequelize-cli

Затем давайте инициализируем проект Sequelize, а затем откроем весь каталог в нашем редакторе кода:

npx sequelize-cli init
code .

Чтобы узнать больше о любой из команд Sequelize CLI ниже, см .:
Начало работы с Sequelize CLI

Давайте настроим наш проект Sequelize для работы с Postgres. Найдите config.json в каталоге /config и замените то, что там, на этот код:

{
  "development": {
    "database": "sequelize_associations_development",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "test": {
    "database": "sequelize_associations_test",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "production": {
    "database": "sequelize_associations_production",
    "host": "127.0.0.1",
    "dialect": "postgres"
  }
}

Круто, теперь мы можем сказать Sequelize создать базу данных:

npx sequelize-cli db:create

Далее мы создадим модель User из командной строки:

npx sequelize-cli model:generate --name User --attributes firstName:string,lastName:string,email:string,password:string

При запуске model:generate автоматически создается и файл модели, и миграция с указанными нами атрибутами. Вы можете найти эти файлы в каталоге своего проекта, но сейчас нет необходимости их менять. (Позже мы отредактируем файл модели, чтобы определить наши связи.)

Теперь мы выполним миграцию, чтобы создать Users таблицу в нашей базе данных:

npx sequelize-cli db:migrate

Теперь давайте создадим исходный файл:

npx sequelize-cli seed:generate --name user

Вы увидите новый файл в /seeders. В этот файл вставьте следующий код, чтобы создать демонстрационного пользователя «John Doe»:

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('Users', [{
        firstName: 'John',
        lastName: 'Doe',
        email: '[email protected]',
        password: '$321!pass!123$',
        createdAt: new Date(),
        updatedAt: new Date()
      }], {});
  },
down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('Users', null, {});
  }
};

После того, как мы сохранили наш исходный файл, давайте выполним его:

npx sequelize-cli db:seed:all

Перейдите в psql и запросите базу данных, чтобы увидеть таблицу Users:

psql sequelize_associations_development
SELECT * FROM "Users";

Определение ассоциаций

Большой! У нас есть рабочая User модель, но нашему Джону Доу, кажется, немного скучно. Давайте дадим Джону работу, создав Task модель:

npx sequelize-cli model:generate --name Task --attributes title:string,userId:integer

Как и в случае с моделью User выше, эта команда CLI Sequelize создаст как файл модели, так и миграцию на основе указанных нами атрибутов. Но на этот раз нам нужно будет отредактировать оба, чтобы связать наши модели вместе.

Сначала найдите task.js в подкаталоге /models в каталоге вашего проекта. Это модель Sequelize для задач, и вы увидите, что метод sequelize.define() устанавливает title и userId как атрибуты, как мы указали выше.

Ниже вы увидите Task.associate. В настоящее время он пуст, но именно здесь мы свяжем каждую задачу с userId. Отредактируйте свой файл, чтобы он выглядел так:

module.exports = (sequelize, DataTypes) => {
  const Task = sequelize.define('Task', {
    title: DataTypes.STRING,
    userId: DataTypes.INTEGER
  }, {});
  Task.associate = function(models) {
    // associations can be defined here
    Task.belongsTo(models.User, {
      foreignKey: 'userId',
      onDelete: 'CASCADE'
    })
  };
  return Task;
};

Что делают эти изменения? Task.belongsTo() устанавливает отношения« принадлежит с моделью User, что означает, что каждая задача будет связана с конкретным пользователем.

Мы делаем это, устанавливая userId как «внешний ключ», что означает, что он относится к ключу в другой модели. В нашей модели задачи должны принадлежать пользователю, поэтому userId будет соответствоватьid в конкретной записи User. (onDelete: 'CASCADE' настраивает нашу модель таким образом, что при удалении пользователя будут удалены и его задачи.)

Нам также необходимо изменить нашу User модель, чтобы отразить другую сторону этих отношений. Найдите user.js и измените раздел под User.associate так, чтобы ваш файл выглядел так:

module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    firstName: DataTypes.STRING,
    lastName: DataTypes.STRING,
    password: DataTypes.STRING,
    email: DataTypes.STRING
  }, {});
  User.associate = function(models) {
    // associations can be defined here
    User.hasMany(models.Task, {
      foreignKey: 'userId',
    })
  };
  return User;
};

Для этой модели мы установили связь «имеет много», что означает, что у пользователя может быть несколько задач. В методе .hasMany() параметр foreignKey устанавливается равным имени ключа в таблице other. Другими словами, когда userId в задаче совпадает с id пользователя, у нас есть совпадение.

Нам все еще нужно внести еще одно изменение, чтобы настроить наши отношения в базе данных. В папке/migrations вашего проекта вы должны увидеть файл, имя которого заканчивается на create-task.js. Измените объект с меткой userId так, чтобы ваш файл выглядел так, как показано ниже:

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('Tasks', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      title: {
        type: Sequelize.STRING
      },
      userId: {
        type: Sequelize.INTEGER,
        onDelete: 'CASCADE',
        references: {
          model: 'Users',
          key: 'id',
          as: 'userId',
        }
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('Tasks');
  }
};

Раздел references настроит Tasks таблицу в нашей базе данных, чтобы отразить те же отношения, которые мы описали выше. Теперь мы можем запустить нашу миграцию:

npx sequelize-cli db:migrate

Теперь наш John Doe готов взять на себя задачи, но у Джона по-прежнему нет никаких реальных задач. Давайте создадим исходный файл задачи:

npx sequelize-cli seed:generate --name task

Найдите только что сгенерированный исходный файл и вставьте следующее, чтобы создать задачу:

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('Tasks', [{
      title: 'Build an app',
      userId: 1,
      createdAt: new Date(),
      updatedAt: new Date()
    }], {});
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('Tasks', null, {});
  }
};

Мы установим userId на 1, чтобы задача принадлежала пользователю, которого мы создали ранее. Теперь мы можем заполнить базу данных.

npx sequelize-cli db:seed:all

Протестируйте базу данных:

psql sequelize_associations_development
SELECT * FROM "Users" JOIN "Tasks" ON "Tasks"."userId" = "Users".id;

Запрос через Sequelize

Теперь мы можем запрашивать информацию в нашей базе данных на основе этих ассоциаций - и с помощью Sequelize мы можем делать это с помощью JavaScript, что упрощает интеграцию с приложением Node.js. Давайте создадим файл для хранения наших запросов:

touch query.js

Вставьте приведенный ниже код в новый файл:

const { User, Task } = require('./models')
const Sequelize = require('sequelize');
const Op = Sequelize.Op

// Find all users with their associated tasks
// Raw SQL: SELECT * FROM "Users" JOIN "Tasks" ON "Tasks"."userId" = "Users".id;

const findAllWithTasks = async () => {
    const users = await User.findAll({
        include: [{
            model: Task
        }]
    });
    console.log("All users with their associated tasks:", JSON.stringify(users, null, 4));
}

const run = async () => {
    await findAllWithTasks()
    await process.exit()
}

run()

Первые три строки выше импортируют наши модели User и Task вместе с Sequelize. После этого мы включаем функцию запроса, которая возвращает каждые User вместе с задачами, связанными с этим пользователем.

Метод .findAll() Sequelize принимает параметры как объект JavaScript. Выше мы использовали параметр include, чтобы воспользоваться преимуществами« быстрой загрузки » - одновременного запроса данных из нескольких моделей. С этой опцией Sequelize вернет объект JavaScript, который включает каждый User со всеми ассоциированными экземплярами Task как вложенными объектами.

Давайте запустим наш файл запроса, чтобы увидеть это в действии:

node query.js

Теперь ясно, что у нашего Джона Доу есть над чем поработать! Мы можем использовать тот же метод для включения User, когда наш запрос находит Task. Вставьте следующий код в query.js:

// Find a task with its associated user
// Raw SQL: SELECT * FROM "Tasks" JOIN "Users" ON "Users"."id" = "Tasks"."userId";

const findTasksWithUser = async () => {
    const tasks = await Task.findAll({
        include: [{
            model: User
        }]
    });
    console.log("All tasks with their associated user:", JSON.stringify(tasks, null, 4));
}

Измените const run внизу query.js, добавив строку для вызова findTasksWithUser(). Теперь снова запустите свой файл в Node - каждый Task должен включать информацию для User, которому он принадлежит.

Запросы в этом пошаговом руководстве используют метод .findAll(). Чтобы узнать больше о других запросах Sequelize, см .: Использование интерфейса командной строки Sequelize и запросов

Вы также можете включить другие параметры рядом с include, чтобы делать более конкретные запросы. Например, ниже мы воспользуемся параметром where, чтобы найти только пользователей с именем John, но по-прежнему возвращать связанные задачи для каждого из них:

// Find all users named John with their associated tasks
// Raw SQL: SELECT * FROM "Users" WHERE firstName = "John" JOIN tasks ON "Tasks"."userId" = "Users".id;
const findAllJohnsWithTasks = async () => {
    const users = await User.findAll({
        where: { firstName: "John" },
        include: [{
            model: Task
        }]
    });
    console.log("All users named John with their associated tasks:", JSON.stringify(users, null, 4));
}

Вставьте указанное выше в свой query.js и измените const run на вызов findAllJohnsWithTasks(), чтобы попробовать.

Теперь, когда вы знаете, как использовать ассоциации моделей в Sequelize, вы можете спроектировать свое приложение для доставки нужных вложенных данных. На следующем этапе вы можете решить включить более надежные исходные данные с помощью Faker или интегрировать приложение Sequelize с Express, чтобы создать сервер Node.js!

Эта статья написана в соавторстве с Джереми Роузом, инженером-программистом, редактором и писателем из Нью-Йорка.

Дополнительная информация о Sequelize CLI:

Ресурсы