Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Представляю вашему вниманию руководство по 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е-дисками и посуточной оплатой у хостинга Маклауд.