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

от автора

Представляю вашему вниманию руководство по 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е-дисками и посуточной оплатой у хостинга Маклауд.

ссылка на оригинал статьи https://habr.com/ru/company/macloud/blog/565062/


Комментарии

Добавить комментарий

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