Pull to refresh

Идеальный инструмент для работы с СУБД без SQL для Node.js или Все, что вы хотели знать о Sequelize. Часть 1

Reading time 22 min
Views 90K

Представляю вашему вниманию руководство по Sequelize.


Sequelize — это ORM (Object-Relational Mapping — объектно-реляционное отображение или преобразование) для работы с такими СУБД (системами управления (реляционными) базами данных, Relational Database Management System, RDBMS), как Postgres, MySQL, MariaDB, SQLite и MSSQL. Это далеко не единственная ORM для работы с названными базами данных (далее — БД), но, на мой взгляд, одна из самых продвинутых и, что называется, "battle tested" (проверенных временем).


ORM хороши тем, что позволяют взаимодействовать с БД на языке приложения (JavaScript), т.е. без использования специально предназначенных для этого языков (SQL). Тем не менее, существуют ситуации, когда запрос к БД легче выполнить с помощью SQL (или можно выполнить только c помощью него). Поэтому перед изучением настоящего руководства рекомендую бросить хотя бы беглый взгляд на SQL. Вот соответствующая шпаргалка.


Это первая из 3 частей руководства, в которой мы поговорим о начале работы с Sequelize, основах создания и использования моделей и экземпляров для взаимодействия с БД, выполнении поисковых и других запросов, геттерах, сеттерах и виртуальных (virtual) атрибутах, валидации, ограничениях и необработанных (raw, SQL) запросах.


Вторая часть.
Третья часть.


Я постараюсь быть максимально лаконичным (надеюсь, без ущерба для полноты изложения материала). Я также постараюсь излагать материал максимально простым языком. Большинство примеров, приводимых в руководстве, заимствованы из официальной документации.


Содержание



Начало работы


Установка


yarn add sequelize
# или
npm i sequelize

Подключение к БД


const { Sequelize } = require('sequelize')

// Вариант 1: передача `URI` для подключения
const sequelize = new Sequelize('sqlite::memory:') // для `sqlite`
const sequelize = new Sequelize('postgres://user:pass@example.com:5432/dbname') // для `postgres`

// Вариант 2: передача параметров по отдельности
const sequelize = new Sequelize({
  dialect: 'sqlite',
  storage: 'path/to/database.sqlite'
})

// Вариант 2: передача параметров по отдельности (для других диалектов)
const sequelize = new Sequelize('database', 'username', 'password', {
  host: 'localhost',
  dialect: /* 'mysql' | 'mariadb' | 'postgres' | 'mssql' */
})

Проверка подключения


try {
  await sequelize.authenticate()
  console.log('Соединение с БД было успешно установлено')
} catch (e) {
  console.log('Невозможно выполнить подключение к БД: ', e)
}

По умолчанию после того, как установки соединения, оно остается открытым. Для его закрытия следует вызвать метод sequelize.close().


↥ Наверх

Модели


Модель — это абстракция, представляющая таблицу в БД.


Модель сообщает Sequelize несколько вещей о сущности (entity), которую она представляет: название таблицы, то, какие колонки она содержит (и их типы данных) и др.


У каждой модели есть название. Это название не обязательно должно совпадать с названием соответствующей таблицы. Обычно, модели именуются в единственном числе (например, User), а таблицы — во множественном (например, Users). Sequelize выполняет плюрализацию (перевод значения из единственного числа во множественное) автоматически.


Модели могут определяться двумя способами:


  • путем вызова sequelize.define(modelName, attributes, options)
  • путем расширения класса Model и вызова init(attributes, options)

После определения, модель доступна через sequelize.model + название модели.


В качестве примера создадим модель User с полями firstName и lastName.


sequelize.define


const { Sequelize, DataTypes } = require('sequelize')
const sequelize = new Sequelize('sqlite::memory:')

const User = sequelize.define(
  'User',
  {
    // Здесь определяются атрибуты модели
    firstName: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    lastName: {
      type: DataTypes.STRING,
      // allowNull по умолчанию имеет значение true
    },
  },
  {
    // Здесь определяются другие настройки модели
  }
)

// `sequelize.define` возвращает модель
console.log(User === sequelize.models.User) // true

Расширение Model


const { Sequelize, DataTypes, Model } = require('sequelize')
const sequelize = new Sequelize('sqlite::memory:')

class User extends Model {}

User.init(
  {
    // Здесь определяются атрибуты модели
    firstName: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    lastName: {
      type: DataTypes.STRING,
    },
  },
  {
    // Здесь определяются другие настройки модели
    sequelize, // Экземпляр подключения (обязательно)
    modelName: 'User', // Название модели (обязательно)
  }
)

console.log(User === sequelize.models.User) // true

sequelize.define под капотом использует Model.init.


В дальнейшем я буду использовать только первый вариант.


Автоматическую плюрализацию названия таблицы можно отключить с помощью настройки freezeTableName:


sequelize.define(
  'User',
  {
    // ...
  },
  {
    freezeTableName: true,
  }
)

или глобально:


const sequelize = new Sequelize('sqlite::memory:', {
  define: {
    freeTableName: true,
  },
})

В этом случае таблица будет называться User.


Название таблицы может определяться в явном виде:


sequelize.define(
  'User',
  {
    // ...
  },
  {
    tableName: 'Employees',
  }
)

В этом случае таблица будет называться Employees.


Синхронизация модели с таблицей:


  • User.sync() — создает таблицу при отсутствии (существующая таблица остается неизменной)
  • User.sync({ force: true }) — удаляет существующую таблицу и создает новую
  • User.sync({ alter: true }) — приводит таблицу в соответствие с моделью

Пример:


// Возвращается промис
await User.sync({ force: true })
console.log('Таблица для модели `User` только что была создана заново!')

Синхронизация всех моделей:


await sequelize.sync({ force: true })
console.log('Все модели были успешно синхронизированы.')

Удаление таблицы:


await User.drop()
console.log('Таблица `User` была удалена.')

Удаление всех таблиц:


await sequelize.drop()
console.log('Все таблицы были удалены.')

Sequelize принимает настройку match с регулярным выражением, позволяющую определять группу синхронизируемых таблиц:


// Выполняем синхронизацию только тех моделей, названия которых заканчиваются на `_test`
await sequelize.sync({ force: true, match: /_test$/ })

Обратите внимание: вместо синхронизации в продакшне следует использовать миграции.


По умолчанию Sequelize автоматически добавляет в создаваемую модель поля createAt и updatedAt с типом DataTypes.DATE. Это можно изменить:


sequelize.define(
  'User',
  {
    // ...
  },
  {
    timestamps: false,
  }
)

Названные поля можно отключать по отдельности и переименовывать:


sequelize.define(
  'User',
  {
    // ...
  },
  {
    timestamps: true,
    // Отключаем `createdAt`
    createdAt: false,
    // Изменяем название `updatedAt`
    updatedAt: 'updateTimestamp',
  }
)

Если для колонки определяется только тип данных, синтаксис определения атрибута может быть сокращен следующим образом:


// до
sequelize.define('User', {
  name: {
    type: DataTypes.STRING,
  },
})

// после
sequelize.define('User', {
  name: DataTypes.STRING,
})

По умолчанию значением колонки является NULL. Это можно изменить с помощью настройки defaultValue (определив "дефолтное" значение):


sequelize.define('User', {
  name: {
    type: DataTypes.STRING,
    defaultValue: 'John Smith',
  },
})

В качестве дефолтных могут использоваться специальные значения:


sequelize.define('Foo', {
  bar: {
    type: DataTypes.DATE,
    // Текущие дата и время, определяемые в момент создания
    defaultValue: Sequelize.NOW,
  },
})

Типы данных


Каждая колонка должна иметь определенный тип данных.


// Импорт встроенных типов данных
const { DataTypes } = require('sequelize')

// Строки
DataTypes.STRING // VARCHAR(255)
DataTypes.STRING(1234) // VARCHAR(1234)
DataTypes.STRING.BINARY // VARCHAR BINARY
DataTypes.TEXT // TEXT
DataTypes.TEXT('tiny') // TINYTEXT
DataTypes.CITEXT // CITEXT - только для `PostgreSQL` и `SQLite`

// Логические значения
DataTypes.BOOLEAN // BOOLEAN

// Числа
DataTypes.INTEGER // INTEGER
DataTypes.BIGINT // BIGINT
DataTypes.BIGINT(11) // BIGINT(11)

DataTypes.FLOAT // FLOAT
DataTypes.FLOAT(11) // FLOAT(11)
DataTypes.FLOAT(11, 10) // FLOAT(11, 10)

DataTypes.REAL // REAL - только для `PostgreSQL`
DataTypes.REAL(11) // REAL(11) - только для `PostgreSQL`
DataTypes.REAL(11, 12) // REAL(11,12) - только для `PostgreSQL`

DataTypes.DOUBLE // DOUBLE
DataTypes.DOUBLE(11) // DOUBLE(11)
DataTypes.DOUBLE(11, 10) // DOUBLE(11, 10)

DataTypes.DECIMAL // DECIMAL
DataTypes.DECIMAL(10, 2) // DECIMAL(10, 2)

// только для `MySQL`/`MariaDB`
DataTypes.INTEGER.UNSIGNED
DataTypes.INTEGER.ZEROFILL
DataTypes.INTEGER.UNSIGNED.ZEROFILL

// Даты
DataTypes.DATE // DATETIME для `mysql`/`sqlite`, TIMESTAMP с временной зоной для `postgres`
DataTypes.DATE(6) // DATETIME(6) для `mysql` 5.6.4+
DataTypes.DATEONLY // DATE без времени

// UUID
DataTypes.UUID

UUID может генерироваться автоматически:


{
  type: DataTypes.UUID,
  defaultValue: Sequelize.UUIDV4
}

Другие типы данных:


// Диапазоны (только для `postgres`)
DataTypes.RANGE(DataTypes.INTEGER) // int4range
DataTypes.RANGE(DataTypes.BIGINT) // int8range
DataTypes.RANGE(DataTypes.DATE) // tstzrange
DataTypes.RANGE(DataTypes.DATEONLY) // daterange
DataTypes.RANGE(DataTypes.DECIMAL) // numrange

// Буферы
DataTypes.BLOB // BLOB
DataTypes.BLOB('tiny') // TINYBLOB
DataTypes.BLOB('medium') // MEDIUMBLOB
DataTypes.BLOB('long') // LONGBLOB

// Перечисления - могут определяться по-другому (см. ниже)
DataTypes.ENUM('foo', 'bar')

// JSON (только для `sqlite`/`mysql`/`mariadb`/`postres`)
DataTypes.JSON

// JSONB (только для `postgres`)
DataTypes.JSONB

// другие
DataTypes.ARRAY(/* DataTypes.SOMETHING */) // массив DataTypes.SOMETHING. Только для `PostgreSQL`

DataTypes.CIDR // CIDR - только для `PostgreSQL`
DataTypes.INET // INET - только для `PostgreSQL`
DataTypes.MACADDR // MACADDR - только для `PostgreSQL`

DataTypes.GEOMETRY // Пространственная колонка. Только для `PostgreSQL` (с `PostGIS`) или `MySQL`
DataTypes.GEOMETRY('POINT') // Пространственная колонка с геометрическим типом. Только для `PostgreSQL` (с `PostGIS`) или `MySQL`
DataTypes.GEOMETRY('POINT', 4326) // Пространственная колонка с геометрическим типом и `SRID`. Только для `PostgreSQL` (с `PostGIS`) или `MySQL`

Настройки колонки


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

sequelize.define('Foo', {
  // Поле `flag` логического типа по умолчанию будет иметь значение `true`
  flag: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },

  // Дефолтным значением поля `myDate` будет текущие дата и время
  myDate: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },

  // Настройка `allowNull` со значением `false` запрещает запись в колонку нулевых значений (NULL)
  title: { type: DataTypes.STRING, allowNull: false },

  // Создание двух объектов с одинаковым набором значений, обычно, приводит к возникновению ошибки.
  // Значением настройки `unique` может быть строка или булевое значение. В данном случае формируется составной уникальный ключ
  uniqueOne: { type: DataTypes.STRING, unique: 'compositeIndex' },
  uniqueTwo: { type: DataTypes.INTEGER, unique: 'compositeIndex' },

  // `unique` используется для обозначения полей, которые должны содержать только уникальные значения
  someUnique: { type: DataTypes.STRING, unique: true },

  // Первичные или основные ключи будут подробно рассмотрены далее
  identifier: { type: DataTypes.STRING, primaryKey: true },

  // Настройка `autoIncrement` может использоваться для создания колонки с автоматически увеличивающимися целыми числами
  incrementMe: { type: DataTypes.INTEGER, autoIncrement: true },

  // Настройка `field` позволяет кастомизировать название колонки
  fieldWithUnderscores: { type: DataTypes.STRING, field: 'field_with_underscores' },

  // Внешние ключи также будут подробно рассмотрены далее
  bar_id: {
    type: DataTypes.INTEGER,

    references: {
      // ссылка на другую модель
      model: Bar,

      // название колонки модели-ссылки с первичным ключом
      key: 'id',

      // в случае с `postres`, можно определять задержку получения внешних ключей
      deferrable: Deferrable.INITIALLY_IMMEDIATE
      /*
        `Deferrable.INITIALLY_IMMEDIATE` - проверка внешних ключей выполняется незамедлительно
        `Deferrable.INITIALLY_DEFERRED` - проверка внешних ключей откладывается до конца транзакции
        `Deferrable.NOT` - без задержки: это не позволит динамически изменять правила в транзакции
      */

      // Комментарии можно добавлять только в `mysql`/`mariadb`/`postres` и `mssql`
      commentMe: {
        type: DataTypes.STRING,
        comment: 'Комментарий'
      }
    }
  }
}, {
  // Аналог атрибута `someUnique`
  indexes: [{
    unique: true,
    fields: ['someUnique']
  }]
})

↥ Наверх

Экземпляры


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


const { Sequelize, DataTypes } = require('sequelize')
const sequelize = new Sequelize('sqlite::memory:')

// Создаем модель для пользователя со следующими атрибутами
const User = sequelize.define('User', {
  // имя
  name: DataTypes.STRING,
  // любимый цвет - по умолчанию зеленый
  favouriteColor: {
    type: DataTypes.STRING,
    defaultValue: 'green',
  },
  // возраст
  age: DataTypes.INTEGER,
  // деньги
  cash: DataTypes.INTEGER,
})

;(async () => {
  // Пересоздаем таблицу в БД
  await sequelize.sync({ force: true })
  // дальнейший код
})()

Создание экземпляра:


// Создаем объект
const jane = User.build({ name: 'Jane' })
// и сохраняем его в БД
await jane.save()

// Сокращенный вариант
const jane = await User.create({ name: 'Jane' })
console.log(jane.toJSON())
console.log(JSON.stringify(jane, null, 2))

Обновление экземпляра:


const john = await User.create({ name: 'John' })
// Вносим изменение
john.name = 'Bob'
// и обновляем соответствующую запись в БД
await john.save()

Удаление экземпляра:


await john.destroy()

"Перезагрузка" экземпляра:


const john = await User.create({ name: 'John' })
john.name = 'Bob'

// Перезагрузка экземпляра приводит к сбросу всех полей к дефолтным значениям
await john.reload()
console.log(john.name) // John

Сохранение отдельных полей:


const john = await User.create({ name: 'John' })
john.name = 'Bob'
john.favouriteColor = 'blue'
// Сохраняем только изменение имени
await john.save({ fields: ['name'] })

await john.reload()
console.log(john.name) // Bob
// Изменение цвета не было зафиксировано
console.log(john.favouriteColor) // green

Автоматическое увеличение значения поля:


const john = await User.create({ name: 'John', age: 98 })

const incrementResult = await john.increment('age', { by: 2 })
// При увеличении значение на 1, настройку `by` можно опустить - increment('age')

// Обновленный пользователь будет возвращен только в `postres`, в других БД он будет иметь значение `undefined`

Автоматическое увеличения значений нескольких полей:


const john = await User.create({ name: 'John', age: 98, cash: 1000 })

await john.increment({
  age: 2,
  cash: 500,
})

Также имеется возможность автоматического уменьшения значений полей (decrement()).


↥ Наверх

Основы выполнения запросов


Создание экземпляра:


const john = await User.create({
  firstName: 'John',
  lastName: 'Smith',
})

Создание экземпляра с определенными полями:


const user = await User.create(
  {
    username: 'John',
    isAdmin: true,
  },
  {
    fields: ['username'],
  }
)

console.log(user.username) // John
console.log(user.isAdmin) // false

Получение экземпляра:


// Получение одного (первого) пользователя
const firstUser = await User.find()

// Получение всех пользователей
const allUsers = await User.findAll() // SELECT * FROM ...;

Выборка полей:


// Получение полей `foo` и `bar`
Model.findAll({
  attributes: ['foo', 'bar'],
}) // SELECT foo, bar FROM ...;

// Изменение имени поля `bar` на `baz`
Model.findAll({
  attributes: ['foo', ['bar', 'baz'], 'qux'],
}) // SELECT foo, bar AS baz, qux FROM ...;

// Выполнение агрегации
// Синоним `n_hats` является обязательным
Model.findAll({
  attributes: [
    'foo',
    [sequelize.fn('COUNT', sequelize.col('hats')), 'n_hats'],
    'bar',
  ],
}) // SELECT foo, COUNT(hats) AS n_hats, bar FROM ...;
// instance.n_hats

// Сокращение - чтобы не перечислять все атрибуты при агрегации
Model.findAll({
  attributes: {
    include: [[sequelize.fn('COUNT', sequelize.col('hats')), 'n_hast']],
  },
})

// Исключение поля из выборки
Model.findAll({
  attributes: {
    exclude: ['baz'],
  },
})

Настройка where позволяет выполнять фильтрацию возвращаемых данных. Существует большое количество операторов, которые могут использоваться совместно с where через Op (см. ниже).


// Выполняем поиск поста по идентификатору его автора
// предполагается `Op.eq`
Post.findAll({
  where: {
    authorId: 2,
  },
}) // SELECT * FROM post WHERE authorId = 2;

// Полный вариант
const { Op } = require('sequelize')
Post.findAll({
  where: {
    authorId: {
      [Op.eq]: 2,
    },
  },
})

// Фильтрация по нескольким полям
// предполагается `Op.and`
Post.findAll({
  where: {
    authorId: 2,
    status: 'active',
  },
}) // SELECT * FROM post WHERE authorId = 2 AND status = 'active';

// Полный вариант
Post.findAll({
  where: {
    [Op.and]: [{ authorId: 2 }, { status: 'active' }],
  },
})

// ИЛИ
Post.findAll({
  where: {
    [Op.or]: [{ authorId: 2 }, { authorId: 3 }],
  },
}) // SELECT * FROM post WHERE authorId = 12 OR authorId = 13;

// Одинаковые названия полей можно опускать
Post.destroy({
  where: {
    authorId: {
      [Op.or]: [2, 3],
    },
  },
}) // DELETE FROM post WHERE authorId = 2 OR authorId = 3;

Операторы


const { Op } = require('sequelize')

Post.findAll({
  where: {
    [Op.and]: [{ a: 1, b: 2 }],   // (a = 1) AND (b = 2)
    [Op.or]: [{ a: 1, b: 2 }],    // (a = 1) OR (b = 2)
    someAttr: {
      // Основные
      [Op.eq]: 3,       // = 3
      [Op.ne]: 4,       // != 4
      [Op.is]: null,    // IS NULL
      [Op.not]: true,   // IS NOT TRUE
      [Op.or]: [5, 6],  // (someAttr = 5) OR (someAttr = 6)

      // Использование диалекта определенной БД (`postgres`, в данном случае)
      [Op.col]: 'user.org_id',    // = 'user'.'org_id'

      // Сравнение чисел
      [Op.gt]: 6,               // > 6
      [Op.gte]: 6,              // >= 6
      [Op.lt]: 7,               // < 7
      [Op.lte]: 7,              // <= 7
      [Op.between]: [8, 10],    // BETWEEN 8 AND 10
      [Op.notBetween]: [8, 10], // NOT BETWEEN 8 AND 10

      // Другие
      [Op.all]: sequelize.literal('SELECT 1'), // > ALL (SELECT 1)

      [Op.in]: [10, 12],    // IN [1, 2]
      [Op.notIn]: [10, 12]  // NOT IN [1, 2]

      [Op.like]: '%foo',      // LIKE '%foo'
      [Op.notLike]: '%foo',   // NOT LIKE '%foo'
      [Op.startsWith]: 'foo', // LIKE 'foo%'
      [Op.endsWith]: 'foo',   // LIKE '%foo'
      [Op.substring]: 'foo',  // LIKE '%foo%'
      [Op.iLike]: '%foo',     // ILIKE '%foo' (учет регистра, только для `postgres`)
      [Op.notILike]: '%foo',        // NOT ILIKE '%foo'
      [Op.regexp]: '^[b|a|r]',      // REGEXP/~ '^[b|a|r]' (только для `mysql`/`postgres`)
      [Op.notRegexp]: '^[b|a|r]',   // NOT REGEXP/!~ '^[b|a|r]' (только для `mysql`/`postgres`),
      [Op.iRegexp]: '^[b|a|r]',     // ~* '^[b|a|r]' (только для `postgres`)
      [Op.notIRegexp]: '^[b|a|r]',  // !~* '^[b|a|r]' (только для `postgres`)

      [Op.any]: [2, 3], // ANY ARRAY[2, 3]::INTEGER (только для `postgres`)

      [Op.like]: { [Op.any]: ['foo', 'bar'] } // LIKE ANY ARRAY['foo', 'bar'] (только для `postgres`)

      // и т.д.
    }
  }
})

Передача массива в where приводит к неявному применению оператора IN:


Post.findAll({
  where: {
    id: [1, 2, 3], // id: { [Op.in]: [1, 2, 3] }
  },
}) // ... WHERE 'post'.'id' IN (1, 2, 3)

Операторы Op.and, Op.or и Op.not могут использоваться для создания сложных операций, связанных с логическими сравнениями:


const { Op } = require('sequelize')

Foo.findAll({
  where: {
    rank: {
      [Op.or]: {
        [Op.lt]: 1000,
        [Op.eq]: null
      }
    }, // rank < 1000 OR rank IS NULL
    {
      createdAt: {
        [Op.lt]: new Date(),
        [Op.gt]: new Date(new Date() - 24 * 60 * 60 * 1000)
      }
    }, // createdAt < [timestamp] AND createdAt > [timestamp]
    {
      [Op.or]: [
        {
          title: {
            [Op.like]: 'Foo%'
          }
        },
        {
          description: {
            [Op.like]: '%foo%'
          }
        }
      ]
    } // title LIKE 'Foo%' OR description LIKE '%foo%'
  }
})

// НЕ
Project.findAll({
  where: {
    name: 'Some Project',
    [Op.not]: [
      { id: [1, 2, 3] },
      {
        description: {
          [Op.like]: 'Awe%'
        }
      }
    ]
  }
})
/*
  SELECT *
  FROM 'Projects'
  WHERE (
    'Projects'.'name' = 'Some Project'
    AND NOT (
      'Projects'.'id' IN (1, 2, 3)
      OR
      'Projects'.'description' LIKE 'Awe%'
    )
  )
*/

"Продвинутые" запросы:


Post.findAll({
  where: sequelize.where(
    sequelize.fn('char_length', sequelize.col('content')),
    7
  ),
}) // WHERE char_length('content') = 7

Post.findAll({
  where: {
    [Op.or]: [
      sequelize.where(sequelize.fn('char_length', sequelize.col('content')), 7),
      {
        content: {
          [Op.like]: 'Hello%',
        },
      },
      {
        [Op.and]: [
          { status: 'draft' },
          sequelize.where(
            sequelize.fn('char_length', sequelize.col('content')),
            {
              [Op.gt]: 8,
            }
          ),
        ],
      },
    ],
  },
})

/*
  ...
  WHERE (
    char_length("content") = 7
    OR
    "post"."content" LIKE 'Hello%'
    OR (
      "post"."status" = 'draft'
      AND
      char_length("content") > 8
    )
  )
*/

Длинное получилось лирическое отступление. Двигаемся дальше.


Обновление экземпляра:


// Изменяем имя пользователя с `userId = 2`
await User.update(
  {
    firstName: 'John',
  },
  {
    where: {
      userId: 2,
    },
  }
)

Удаление экземпляра:


// Удаление пользователя с `id = 2`
await User.destroy({
  where: {
    userId: 2,
  },
})

// Удаление всех пользователей
await User.destroy({
  truncate: true,
})

Создание нескольких экземпляров одновременно:


const users = await User.bulkCreate([{ name: 'John' }, { name: 'Jane' }])

// Настройка `validate` со значением `true` заставляет `Sequelize` выполнять валидацию каждого объекта, создаваемого с помощью `bulkCreate()`
// По умолчанию валидация таких объектов не проводится
const User = sequelize.define('User', {
  name: {
    type: DataTypes.STRING,
    validate: {
      len: [2, 10],
    },
  },
})

await User.bulkCreate([{ name: 'John' }, { name: 'J' }], { validate: true }) // Ошибка!

// Настройка `fields` позволяет определять поля для сохранения
await User.bulkCreate([{ name: 'John' }, { name: 'Jane', age: 30 }], {
  fields: ['name'],
}) // Сохраняем только имена пользователей

Сортировка и группировка


Настройка order определяет порядок сортировки возвращаемых объектов:


Submodel.findAll({
  order: [
    // Сортировка по заголовку (по убыванию)
    ['title', 'DESC'],

    // Сортировка по максимальному возврасту
    sequelize.fn('max', sequelize.col('age')),

    // Тоже самое, но по убыванию
    [sequelize.fn('max', sequelize.col('age')), 'DESC'],

    // Сортировка по `createdAt` из связанной модели
    [Model, 'createdAt', 'DESC'],

    // Сортировка по `createdAt` из двух связанных моделей
    [Model, AnotherModel, 'createdAt', 'DESC'],

    // и т.д.
  ],

  // Сортировка по максимальному возврасту (по убыванию)
  order: sequelize.literal('max(age) DESC'),

  // Сортировка по максимальному возрасту (по возрастанию - направление сортировки по умолчанию)
  order: sequelize.fn('max', sequelize.col('age')),

  // Сортировка по возрасту (по возрастанию)
  order: sequelize.col('age'),

  // Случайная сортировка
  order: sequelize.random(),
})

Model.findOne({
  order: [
    // возвращает `name`
    ['name'],
    // возвращает `'name' DESC`
    ['name', 'DESC'],
    // возвращает `max('age')`
    sequelize.fn('max', sequelize.col('age')),
    // возвращает `max('age') DESC`
    [sequelize.fn('max', sequelize.col('age')), 'DESC'],

    // и т.д.
  ],
})

Синтаксис группировки идентичен синтаксису сортировки, за исключением того, что при группировке не указывается направление. Кроме того, синтаксис группировки может быть сокращен до строки:


Project.findAll({ group: 'name' }) // GROUP BY name

Настройки limit и offset позволяют ограничивать и/или пропускать определенное количество возвращаемых объектов:


// Получаем 10 проектов
Project.findAll({ limit: 10 })

// Пропускаем 5 первых объектов
Project.findAll({ offset: 5 })

// Пропускаем 5 первых объектов и возвращаем 10
Project.findAll({ offset: 5, limit: 10 })

Sequelize предоставляет несколько полезных утилит:


// Определяем число вхождений
console.log(
  `В настоящий момент в БД находится ${await Project.count()} проектов.`
)

const amount = await Project.count({
  where: {
    projectId: {
      [Op.gt]: 25,
    },
  },
})
console.log(
  `В настоящий момент в БД находится ${amount} проектов с идентификатором больше 25.`
)

// max, min, sum
// Предположим, что у нас имеется 3 пользователя 20, 30 и 40 лет
await User.max('age') // 40
await User.max('age', { where: { age: { [Op.lt]: 31 } } }) // 30
await User.min('age') // 20
await User.min('age', { where: { age: { [Op.gt]: 21 } } }) // 30
await User.sum('age') // 90
await User.sum('age', { where: { age: { [op.gt]: 21 } } }) // 70

↥ Наверх

Поисковые запросы


Настройка raw со значением true отключает "оборачивание" ответа, возвращаемого SELECT, в экземпляр модели.


  • findAll() — возвращает все экземпляры модели
  • findByPk() — возвращает один экземпляр по первичному ключу

const project = await Project.findByPk(123)

  • findOne() — возвращает первый или один экземпляр модели (это зависит от того, указано ли условие для поиска)

const project = await Project.findOne({ where: { projectId: 123 } })

  • findOrCreate() — возвращает или создает и возвращает экземпляр, а также логическое значение — индикатор создания экземпляра. Настройка defaults используется для определения значений по умолчанию. При ее отсутствии, для заполнения полей используется значение, указанное в условии

// Предположим, что у нас имеется пустая БД с моделью `User`, у которой имеются поля `username` и `job`
const [user, created] = await User.findOrCreate({
  where: { username: 'John' },
  defaults: {
    job: 'JavaScript Developer',
  },
})

  • findAndCountAll() — комбинация findAll() и count. Может быть полезным при использовании настроек limit и offset, когда мы хотим знать точное число записей, совпадающих с запросом. Возвращает объект с двумя свойствами:
    • count — количество записей, совпадающих с запросом (целое число)
    • rows — массив объектов

const { count, rows } = await Project.findAndCountAll({
  where: {
    title: {
      [Op.like]: 'foo%',
    },
  },
  offset: 10,
  limit: 5,
})

↥ Наверх

Геттеры, сеттеры и виртуальные атрибуты


Sequelize позволяет определять геттеры и сеттеры для атрибутов моделей, а также виртуальные атрибуты — атрибуты, которых не существует в таблице и которые заполняются или наполняются (имеется ввиду популяция) Serquelize автоматически. Последние могут использоваться, например, для упрощения кода.


Геттер — это функция get(), определенная для колонки:


const User = sequelize.define('User', {
  username: {
    type: DataTypes.STRING,
    get() {
      const rawValue = this.getDataValue(username)
      return rawValue ? rawValue.toUpperCase() : null
    },
  },
})

Геттер вызывается автоматически при чтении поля.


Обратите внимание: для получения значения поля в геттере мы использовали метод getDataValue(). Если вместо этого указать this.username, то мы попадем в бесконечный цикл.


Сеттер — это функция set(), определенная для колонки. Она принимает значение для установки:


const User = sequelize.define('user', {
  username: DataTypes.STRING,
  password: {
    type: DataTypes.STRING,
    set(value) {
      // Перед записью в БД пароли следует "хэшировать" с помощью криптографической функции
      this.setDataValue('password', hash(value))
    },
  },
})

Сеттер вызывается автоматически при создании экземпляра.


В сеттере можно использовать значения других полей:


const User = sequelize.define('User', {
  username: DatTypes.STRING,
  password: {
    type: DataTypes.STRING,
    set(value) {
      // Используем значение поля `username`
      this.setDataValue('password', hash(this.username + value))
    },
  },
})

Геттеры и сеттеры можно использовать совместно. Допустим, что у нас имеется модель Post с полем content неограниченной длины, и в целях экономии памяти мы решили хранить в БД содержимое поста в сжатом виде. Обратите внимание: многие современные БД выполняют сжатие (компрессию) данных автоматически.


const { gzipSync, gunzipSync } = require('zlib')

const Post = sequelize.define('post', {
  content: {
    type: DataTypes.TEXT,
    get() {
      const storedValue = this.getDataValue('content')
      const gzippedBuffer = Buffer.from(storedValue, 'base64')
      const unzippedBuffer = gunzipSync(gzippedBuffer)
      return unzippedBuffer.toString()
    },
    set(value) {
      const gzippedBuffer = gzipSync(value)
      this.setDataValue('content', gzippedBuffer.toString('base64'))
    },
  },
})

Представим, что у нас имеется модель User с полями firstName и lastName, и мы хотим получать полное имя пользователя. Для этого мы можем создать виртуальный атрибут со специальным типом DataTypes.VIRTUAL:


const User = sequelize.define('user', {
  firstName: DataTypes.STRING,
  lastName: DataTypes.STRING,
  fullName: {
    type: DataTypes.VIRTUAL,
    get() {
      return `${this.firstName} ${this.lastName}`
    },
    set(value) {
      throw new Error('Нельзя этого делать!')
    },
  },
})

В таблице не будет колонки fullName, однако мы сможем получать значение этого поля, как если бы оно существовало на самом деле.


↥ Наверх

Валидация и ограничения


Наша моделька будет выглядеть так:


const { Sequelize, Op, DataTypes } = require('sequelize')
const sequelize = new Sequelize('sqlite::memory:')

const User = sequelize.define('user', {
  username: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
  },
  hashedPassword: {
    type: DataTypes.STRING(64),
    is: /^[0-9a-f]{64}$/i,
  },
})

Отличие между выполнением валидации и применением или наложением органичение на значение поля состоит в следующем:


  • валидация выполняется на уровне Sequelize; для ее выполнения можно использовать любую функцию, как встроенную, так и кастомную; при провале валидации, SQL-запрос в БД не отправляется;
  • ограничение определяется на уровне SQL; примером ограничения является настройка unique; при провале ограничения, запрос в БД все равно отправляется

В приведенном примере мы ограничили уникальность имени пользователя с помощью настройки unique. При попытке записать имя пользователя, которое уже существует в БД, возникнет ошибка SequelizeUniqueConstraintError.


По умолчанию колонки таблицы могут быть пустыми (нулевыми). Настройка allowNull со значением false позволяет это запретить. Обратите внимание: без установки данной настройки хотя бы для одного поля, можно будет выполнить такой запрос: User.create({}).


Валидаторы позволяют проводить проверку в отношении каждого атрибута модели. Валидация автоматически выполняется при запуске методов create(), update() и save(). Ее также можно запустить вручную с помощью validate().


Как было отмечено ранее, мы можем определять собственные валидаторы или использовать встроенные (предоставляемые библиотекой validator.js).


sequelize.define('foo', {
  bar: {
    type: DataTypes.STRING,
    validate: {
      is: /^[a-z]+$/i, // определение совпадения с регулярным выражением
      not: /^[a-z]+$/i, // определение отсутствия совпадения с регуляркой
      isEmail: true,
      isUrl: true,
      isIP: true,
      isIPv4: true,
      isIPv6: true,
      isAlpha: true,
      isAlphanumeric: true,
      isNumeric: true,
      isInt: true,
      isFloat: true,
      isDecimal: true,
      isLowercase: true,
      isUppercase: true,
      notNull: true,
      isNull: true,
      notEmpty: true,
      equals: 'определенное значение',
      contains: 'foo', // определение наличия подстроки
      notContains: 'bar', // определение отсутствия подстроки
      notIn: [['foo', 'bar']], // определение того, что значение НЕ является одним из указанных
      isIn: [['foo', 'bar']], // определение того, что значение является одним из указанных
      len: [2, 10], // длина строки должна составлять от 2 до 10 символов
      isUUID: true,
      isDate: true,
      isAfter: '2021-06-12',
      isBefore: '2021-06-15',
      max: 65,
      min: 18,
      isCreditCard: true,

      // Примеры кастомных валидаторов
      isEven(value) {
        if (parseInt(value) % 2 !== 0) {
          throw new Error('Разрешены только четные числа!')
        }
      },
      isGreaterThanOtherField(value) {
        if (parseInt(value) < parseInt(this.otherField)) {
          throw new Error(
            `Значение данного поля должно быть больше значения ${otherField}!`
          )
        }
      },
    },
  },
})

Для кастомизации сообщения об ошибке можно использовать объект со свойством msg:


isInt: {
  msg: 'Значение должно быть целым числом!'
}

В этом случае для указания аргументов используется свойство args:


isIn: {
  args: [['ru', 'en']],
  msg: 'Язык должен быть русским или английским!'
}

Для поля, которое может иметь значение null, встроенные валидаторы пропускаются. Это означает, что мы, например, можем определить поле, которое либо должно содержать строку длиной 5-10 символов, либо должно быть пустым:


const User = sequelize.define('user', {
  username: {
    type: DataTypes.STRING,
    allowNull: true,
    validate: {
      len: [5, 10],
    },
  },
})

Обратите внимание, что для нулевых полей кастомные валидаторы выполняются:


const User = sequelize.define('user', {
  age: DataTypes.INTEGER,
  name: {
    type: DataTypes.STRING,
    allowNull: true,
    validate: {
      customValidator(value) {
        if (value === null && this.age < 18) {
          throw new Error('Нулевые значения разрешены только совершеннолетним!')
        }
      },
    },
  },
})

Мы можем выполнять валидацию не только отдельных полей, но и модели в целом. В следующем примере мы проверяем наличие или отсутствии как поля latitude, так и поля longitude (либо должны быть указаны оба поля, либо не должно быть указано ни одного):


const Place = sequelize.define(
  'place',
  {
    name: DataTypes.STRING,
    address: DataTypes.STRING,
    latitude: {
      type: DataTypes.INTEGER,
      validate: {
        min: -90,
        max: 90,
      },
    },
    longitude: {
      type: DataTypes.INTEGER,
      validate: {
        min: -180,
        max: 180,
      },
    },
  },
  {
    validate: {
      bothCoordsOrNone() {
        if (!this.latitude !== !this.longitude) {
          throw new Error(
            'Либо укажите и долготу, и широту, либо ничего не указывайте!'
          )
        }
      },
    },
  }
)

↥ Наверх

Необработанные запросы


sequelize.query() позволяет выполнять необработанные SQL-запросы (raw queries). По умолчанию данная функция возвращает массив с результатами и объект с метаданными, при этом, содержание последнего зависит от используемого диалекта.


const [results, metadata] = await sequelize.query(
  "UPDATE users SET username = 'John' WHERE userId = 123"
)

Если нам не нужны метаданные, для правильного форматирования результата можно воспользоваться специальными типами запроса (query types):


const { QueryTypes } = require('sequelize')

const users = await sequelize.query('SELECT * FROM users', {
  // тип запроса - выборка
  type: QueryTypes.SELECT,
})

Для привязки результатов необработанного запроса к модели используются настройки model и, опционально, mapToModel:


const projects = await sequelize.query('SELECT * FROM projects', {
  model: Project,
  mapToModel: true,
})

Пример использования других настроек:


sequelize.query('SELECT 1', {
  // "логгирование" - функция или `false`
  logging: console.log,

  // если `true`, возвращается только первый результат
  plain: false,

  // если `true`, для выполнения запроса не нужна модель
  raw: false,

  // тип выполняемого запроса
  type: QueryTypes.SELECT,
})

Если название атрибута в таблице содержит точки, то результирующий объект может быть преобразован во вложенные объекты с помощью настройки nest.


Без nest: true:


const records = await sequelize.query('SELECT 1 AS `foo.bar.baz`', {
  type: QueryTypes.SELECT,
})
console.log(JSON.stringify(records[0], null, 2))
// { 'foo.bar.baz': 1 }

С nest: true:


const records = await sequelize.query('SELECT 1 AS `foo.bar.baz`', {
  type: QueryTypes.SELECT,
  nest: true,
})
console.log(JSON.stringify(records[0], null, 2))
/*
{
  'foo': {
    'bar': {
      'baz': 1
    }
  }
}
*/

Замены при выполнении запроса могут производиться двумя способами:


  • с помощью именованных параметров (начинающихся с :)
  • с помощью неименованных параметров (представленных ?)

Заменители (placeholders) передаются в настройку replacements в виде массива (для неименованных параметров) или в виде объекта (для именованных параметров):


  • если передан массив, ? заменяется элементами массива в порядке их следования
  • если передан объект, :key заменяются ключами объекта. При отсутствии в объекте ключей для заменяемых значений, а также в случае, когда ключей в объекте больше, чем заменяемых значений, выбрасывается исключение

sequelize.query('SELECT * FROM projects WHERE status = ?', {
  replacements: ['active'],
  type: QueryTypes.SELECT,
})

sequelize.query('SELECT * FROM projects WHERE status = :status', {
  replacements: { status: 'active' },
  type: QueryTypes.SELECT,
})

Продвинутые примеры замены:


// Замена производится при совпадении с любым значением из массива
sequelize.query('SELECT * FROM projects WHERE status IN(:status)', {
  replacements: { status: ['active', 'inactive'] },
  type: QueryTypes.SELECT,
})

// Замена выполняется для всех пользователей, имена которых начинаются с `J`
sequelize.query('SELECT * FROM users WHERE name LIKE :search_name', {
  replacements: { search_name: 'J%' },
  type: QueryTypes.SELECT,
})

Кроме замены, можно выполнять привязку (bind) параметров. Привязка похожа на замену, но заменители обезвреживаются (escaped) и вставляются в запрос, отправляемый в БД, а связанные параметры отправляются в БД по отдельности. Связанные параметры обозначаются с помощью $число или $строка:


  • если передан массив, $1 будет указывать на его первый элемент (bind[0])
  • если передан объект, $key будет указывать на object['key']. Каждый ключ объекта должен начинаться с буквы. $1 является невалидным ключом, даже если существует object['1']
  • в обоих случаях для сохранения знака $ может использоваться $$

Связанные параметры не могут быть ключевыми словами SQL, названиями таблиц или колонок. Они игнорируются внутри текста, заключенного в кавычки. Кроме того, в postgres может потребоваться указывать тип связываемого параметра в случае, когда он не может быть выведен на основании контекста — $1::varchar.


sequelize.query(
  'SELECT *, "текст с литеральным $$1 и литеральным $$status" AS t FROM projects WHERE status = $1',
  {
    bind: ['active'],
    type: QueryTypes.SELECT,
  }
)

sequelize.query(
  'SELECT *, "текст с литеральным $$1 и литеральным $$status" AS t FROM projects WHERE status = $status',
  {
    bind: { status: 'active' },
    type: QueryTypes.SELECT,
  }
)

↥ Наверх

На этом первая часть руководства завершена. В следующей части мы поговорим о простых и продвинутых ассоциациях (отношениях между моделями), "параноике", нетерпеливой и ленивой загрузке, а также о многом другом.




Аренда VPS/VDS с быстрыми NVMе-дисками и посуточной оплатой у хостинга Маклауд.


Tags:
Hubs:
+25
Comments 28
Comments Comments 28

Articles