Sequelize - это ORM для работы с такими реляционными базами данных как Postgres, MySQL, MariaDB, SQLite и MSSQL. На сегодняшний день это самое популярное решений для Node.js.
const{Sequelize}=require('sequelize');// Вариант 1: передача `URI` для подключенияconstsequelize=newSequelize('sqlite::memory:');// для `sqlite`constsequelize=newSequelize('postgres://user:[email protected]:5432/dbname');// для `postgres`// Вариант 2: передача параметров по отдельностиconstsequelize=newSequelize({dialect:'sqlite',storage:'path/to/database.sqlite',});// Вариант 2: передача параметров по отдельности (для других диалектов)constsequelize=newSequelize('database','username','password',{host:'localhost',dialect:''/* 'mysql' | 'mariadb' | 'postgres' | 'mssql' */,});
Проверка подключения
123456789
try{awaitsequelize.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
1 2 3 4 5 6 7 8 91011121314151617181920212223
const{Sequelize,DataTypes}=require('sequelize');constsequelize=newSequelize('sqlite::memory:');constUser=sequelize.define('User',{// Здесь определяются атрибуты моделиfirstName:{type:DataTypes.STRING,allowNull:false,},lastName:{type:DataTypes.STRING,// allowNull по умолчанию имеет значение true},},{// Здесь определяются другие настройки модели});// `sequelize.define` возвращает модельconsole.log(User===sequelize.models.User);// true
const{Sequelize,DataTypes,Model,}=require('sequelize');constsequelize=newSequelize('sqlite::memory:');classUserextendsModel{}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:
// Импорт встроенных типов данныхconst{DataTypes}=require('sequelize');// СтрокиDataTypes.STRING;// VARCHAR(255)DataTypes.STRING(1234);// VARCHAR(1234)DataTypes.STRING.BINARY;// VARCHAR BINARYDataTypes.TEXT;// TEXTDataTypes.TEXT('tiny');// TINYTEXTDataTypes.CITEXT;// CITEXT - только для `PostgreSQL` и `SQLite`// Логические значенияDataTypes.BOOLEAN;// BOOLEAN// ЧислаDataTypes.INTEGER;// INTEGERDataTypes.BIGINT;// BIGINTDataTypes.BIGINT(11);// BIGINT(11)DataTypes.FLOAT;// FLOATDataTypes.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;// DOUBLEDataTypes.DOUBLE(11);// DOUBLE(11)DataTypes.DOUBLE(11,10);// DOUBLE(11, 10)DataTypes.DECIMAL;// DECIMALDataTypes.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 без времени// UUIDDataTypes.UUID;
// Диапазоны (только для `postgres`)DataTypes.RANGE(DataTypes.INTEGER);// int4rangeDataTypes.RANGE(DataTypes.BIGINT);// int8rangeDataTypes.RANGE(DataTypes.DATE);// tstzrangeDataTypes.RANGE(DataTypes.DATEONLY);// daterangeDataTypes.RANGE(DataTypes.DECIMAL);// numrange// БуферыDataTypes.BLOB;// BLOBDataTypes.BLOB('tiny');// TINYBLOBDataTypes.BLOB('medium');// MEDIUMBLOBDataTypes.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']}]})
Наш начальный код будет выглядеть следующим образом:
1 2 3 4 5 6 7 8 91011121314151617181920212223
const{Sequelize,DataTypes}=require('sequelize');constsequelize=newSequelize('sqlite::memory:');// Создаем модель для пользователя со следующими атрибутамиconstUser=sequelize.define('User',{// имяname:DataTypes.STRING,// любимый цвет - по умолчанию зеленыйfavouriteColor:{type:DataTypes.STRING,defaultValue:'green',},// возрастage:DataTypes.INTEGER,// деньгиcash:DataTypes.INTEGER,});(async()=>{// Пересоздаем таблицу в БДawaitsequelize.sync({force:true});// дальнейший код})();
Создание экземпляра:
123456789
// Создаем объектconstjane=User.build({name:'Jane'});// и сохраняем его в БДawaitjane.save();// Сокращенный вариантconstjane=awaitUser.create({name:'Jane'});console.log(jane.toJSON());console.log(JSON.stringify(jane,null,2));
Обновление экземпляра:
12345
constjohn=awaitUser.create({name:'John'});// Вносим изменениеjohn.name='Bob';// и обновляем соответствующую запись в БДawaitjohn.save();
Удаление экземпляра:
1
awaitjohn.destroy();
"Перезагрузка" экземпляра:
123456
constjohn=awaitUser.create({name:'John'});john.name='Bob';// Перезагрузка экземпляра приводит к сбросу всех полей к дефолтным значениямawaitjohn.reload();console.log(john.name);// John
Сохранение отдельных полей:
1 2 3 4 5 6 7 8 910
constjohn=awaitUser.create({name:'John'});john.name='Bob';john.favouriteColor='blue';// Сохраняем только изменение имениawaitjohn.save({fields:['name']});awaitjohn.reload();console.log(john.name);// Bob// Изменение цвета не было зафиксированоconsole.log(john.favouriteColor);// green
Автоматическое увеличение значения поля:
12345678
constjohn=awaitUser.create({name:'John',age:98});constincrementResult=awaitjohn.increment('age',{by:2,});// При увеличении значение на 1, настройку `by` можно опустить - increment('age')// Обновленный пользователь будет возвращен только в `postres`, в других БД он будет иметь значение `undefined`
Автоматическое увеличения значений нескольких полей:
// Получение одного (первого) пользователяconstfirstUser=awaitUser.find();// Получение всех пользователейconstallUsers=awaitUser.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:
12345
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]:newDate(),[Op.gt]:newDate(newDate()-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') = 7Post.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 ) )*/
constusers=awaitUser.bulkCreate([{name:'John'},{name:'Jane'},]);// Настройка `validate` со значением `true` заставляет `Sequelize` выполнять валидацию каждого объекта, создаваемого с помощью `bulkCreate()`// По умолчанию валидация таких объектов не проводитсяconstUser=sequelize.define('User',{name:{type:DataTypes.STRING,validate:{len:[2,10],},},});awaitUser.bulkCreate([{name:'John'},{name:'J'}],{validate:true,});// Ошибка!// Настройка `fields` позволяет определять поля для сохраненияawaitUser.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'],// и т.д.],});
Синтаксис группировки идентичен синтаксису сортировки, за исключением того, что при группировке не указывается направление. Кроме того, синтаксис группировки может быть сокращен до строки:
1
Project.findAll({group:'name'});// GROUP BY name
Настройки limit и offset позволяют ограничивать и/или пропускать определенное количество возвращаемых объектов:
12345678
// Получаем 10 проектовProject.findAll({limit:10});// Пропускаем 5 первых объектовProject.findAll({offset:5});// Пропускаем 5 первых объектов и возвращаем 10Project.findAll({offset:5,limit:10});
Sequelize предоставляет несколько полезных утилит:
1 2 3 4 5 6 7 8 9101112131415161718192021222324
// Определяем число вхожденийconsole.log(`В настоящий момент в БД находится ${awaitProject.count()} проектов.`);constamount=awaitProject.count({where:{projectId:{[Op.gt]:25,},},});console.log(`В настоящий момент в БД находится ${amount} проектов с идентификатором больше 25.`);// max, min, sum// Предположим, что у нас имеется 3 пользователя 20, 30 и 40 летawaitUser.max('age');// 40awaitUser.max('age',{where:{age:{[Op.lt]:31}}});// 30awaitUser.min('age');// 20awaitUser.min('age',{where:{age:{[Op.gt]:21}}});// 30awaitUser.sum('age');// 90awaitUser.sum('age',{where:{age:{[op.gt]:21}}});// 70
findOrCreate() - возвращает или создает и возвращает экземпляр, а также логическое значение - индикатор создания экземпляра. Настройка defaults используется для определения значений по умолчанию. При ее отсутствии, для заполнения полей используется значение, указанное в условии
1234567
// Предположим, что у нас имеется пустая БД с моделью `User`, у которой имеются поля `username` и `job`const[user,created]=awaitUser.findOrCreate({where:{username:'John'},defaults:{job:'JavaScript Developer',},});
findAndCountAll() - комбинация findAll() и count. Может быть полезным при использовании настроек limit и offset, когда мы хотим знать точное число записей, совпадающих с запросом. Возвращает объект с двумя свойствами:
count - количество записей, совпадающих с запросом (целое число)
Sequelize позволяет определять геттеры и сеттеры для атрибутов моделей, а также виртуальные атрибуты - атрибуты, которых не существует в таблице и которые заполняются или наполняются (имеется ввиду популяция) Serquelize автоматически. Последние могут использоваться, например, для упрощения кода.
Геттер - это функция get(), определенная для колонки:
Обратите внимание: для получения значения поля в геттере мы использовали метод getDataValue(). Если вместо этого указать this.username, то мы попадем в бесконечный цикл.
Сеттер - это функция set(), определенная для колонки. Она принимает значение для установки:
1 2 3 4 5 6 7 8 910
constUser=sequelize.define('user',{username:DataTypes.STRING,password:{type:DataTypes.STRING,set(value){// Перед записью в БД пароли следует "хэшировать" с помощью криптографической функцииthis.setDataValue('password',hash(value));},},});
Сеттер вызывается автоматически при создании экземпляра.
В сеттере можно использовать значения других полей:
1 2 3 4 5 6 7 8 910111213
constUser=sequelize.define('User',{username:DatTypes.STRING,password:{type:DataTypes.STRING,set(value){// Используем значение поля `username`this.setDataValue('password',hash(this.username+value));},},});
Геттеры и сеттеры можно использовать совместно. Допустим, что у нас имеется модель Post с полем content неограниченной длины, и в целях экономии памяти мы решили хранить в БД содержимое поста в сжатом виде. Обратите внимание: многие современные БД выполняют сжатие (компрессию) данных автоматически.
Представим, что у нас имеется модель User с полями firstName и lastName, и мы хотим получать полное имя пользователя. Для этого мы можем создать виртуальный атрибут со специальным типом DataTypes.VIRTUAL:
1 2 3 4 5 6 7 8 910111213
constUser=sequelize.define('user',{firstName:DataTypes.STRING,lastName:DataTypes.STRING,fullName:{type:DataTypes.VIRTUAL,get(){return`${this.firstName}${this.lastName}`;},set(value){thrownewError('Нельзя этого делать!');},},});
В таблице не будет колонки fullName, однако мы сможем получать значение этого поля, как если бы оно существовало на самом деле.
Отличие между выполнением валидации и применением или наложением органичение на значение поля состоит в следующем:
валидация выполняется на уровне 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){thrownewError('Разрешены только четные числа!');}},isGreaterThanOtherField(value){if(parseInt(value)<parseInt(this.otherField)){thrownewError(`Значение данного поля должно быть больше значения ${otherField}!`);}},},},});
Для кастомизации сообщения об ошибке можно использовать объект со свойством msg:
123
isInt:{msg:'Значение должно быть целым числом!';}
В этом случае для указания аргументов используется свойство args:
1234
isIn:{args:[['ru','en']],msg:'Язык должен быть русским или английским!'}
Для поля, которое может иметь значение null, встроенные валидаторы пропускаются. Это означает, что мы, например, можем определить поле, которое либо должно содержать строку длиной 5-10 символов, либо должно быть пустым:
Обратите внимание, что для нулевых полей кастомные валидаторы выполняются:
1 2 3 4 5 6 7 8 910111213141516
constUser=sequelize.define('user',{age:DataTypes.INTEGER,name:{type:DataTypes.STRING,allowNull:true,validate:{customValidator(value){if(value===null&&this.age<18){thrownewError('Нулевые значения разрешены только совершеннолетним!');}},},},});
Мы можем выполнять валидацию не только отдельных полей, но и модели в целом. В следующем примере мы проверяем наличие или отсутствии как поля latitude, так и поля longitude (либо должны быть указаны оба поля, либо не должно быть указано ни одного):
constPlace=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){thrownewError('Либо укажите и долготу, и широту, либо ничего не указывайте!');}},},});
sequelize.query() позволяет выполнять необработанные SQL-запросы (raw queries). По умолчанию данная функция возвращает массив с результатами и объект с метаданными, при этом, содержание последнего зависит от используемого диалекта.
123
const[results,metadata]=awaitsequelize.query("UPDATE users SET username = 'John' WHERE userId = 123");
Если нам не нужны метаданные, для правильного форматирования результата можно воспользоваться специальными типами запроса (query types):
123456
const{QueryTypes}=require('sequelize');constusers=awaitsequelize.query('SELECT * FROM users',{// тип запроса - выборкаtype:QueryTypes.SELECT,});
Для привязки результатов необработанного запроса к модели используются настройки model и, опционально, mapToModel:
1234567
constprojects=awaitsequelize.query('SELECT * FROM projects',{model:Project,mapToModel:true,});
Пример использования других настроек:
1 2 3 4 5 6 7 8 910111213
sequelize.query('SELECT 1',{// "логгирование" - функция или `false`logging:console.log,// если `true`, возвращается только первый результатplain:false,// если `true`, для выполнения запроса не нужна модельraw:false,// тип выполняемого запросаtype:QueryTypes.SELECT,});
Если название атрибута в таблице содержит точки, то результирующий объект может быть преобразован во вложенные объекты с помощью настройки nest.
Без nest: true:
12345678
constrecords=awaitsequelize.query('SELECT 1 AS `foo.bar.baz`',{type:QueryTypes.SELECT,});console.log(JSON.stringify(records[0],null,2));// { 'foo.bar.baz': 1 }
Замены при выполнении запроса могут производиться двумя способами:
с помощью именованных параметров (начинающихся с :)
с помощью неименованных параметров (представленных ?)
Заменители (placeholders) передаются в настройку replacements в виде массива (для неименованных параметров) или в виде объекта (для именованных параметров):
если передан массив, ? заменяется элементами массива в порядке их следования
если передан объект, :key заменяются ключами объекта. При отсутствии в объекте ключей для заменяемых значений, а также в случае, когда ключей в объекте больше, чем заменяемых значений, выбрасывается исключение
1 2 3 4 5 6 7 8 9101112
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,});
Продвинутые примеры замены:
1 2 3 4 5 6 7 8 91011121314151617
// Замена производится при совпадении с любым значением из массива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.
1 2 3 4 5 6 7 8 9101112131415
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,});
Sequelize поддерживает стандартные ассоциации или отношения между моделями: один-к-одному (One-To-One), один-ко-многим (One-To-Many), многие-ко-многим (Many-To-Many).
Существует 4 типа ассоциаций:
HasOne
BelongsTo
HasMany
BelongsToMany
Определение ассоциации
Предположим, что у нас имеется 2 модели, A и B. Вот как можно определить между ними связь:
Все эти функции принимают объект с настройками (для первых трех он является опциональным, для последнего - обязательным). В настройках должно быть определено как минимум свойство through:
1 2 3 4 5 6 7 8 910
A.hasOne(B,{/* настройки */});A.belongsTo(B,{/* настройки */});A.hasMany(B,{/* настройки */});A.belongsToMany(B,{through:'C'/* другие настройки */});
Порядок определения ассоциаций имеет принципиальное значение. В приведенных примерах A - это модель-источник (source), а B - это целевая модель (target). Запомните эти термины.
A.hasOne(B) означает, что между A и B существуют отношения один-к-одному, при этом, внешний ключ (foreign key) определяется в целевой модели (B).
A.belongsTo(B) - отношения один-к-одному, внешний ключ определяется в источнике (A).
A.hasMany(B) - отношения один-ко-многим, внешний ключ определяется в целевой модели (B).
В этих случаях Sequelize автоматически добавляет внешние ключи (при их отсутствии) в соответствующие модели (таблицы).
A.belongsToMany(B, { through: 'C' }) означает, что между A и B существуют отношения многие-ко-многим, таблица C выступает в роли связующего звена между ними через внешние ключи (например, aId и bId). Sequelize автоматически создает модель C при ее отсутствии, определяя в ней соответствующие ключи.
Определение стандартных отношений
Как правило, ассоциации используются парами:
для создания отношений один-к-одному используются hasOne() и belongsTo()
для один-ко-многим - hasMany() и belongsTo()
для многие-ко-многим - два belongsToMany(). Обратите внимание, что существуют также отношения "супер многие-ко-многим", где одновременно используется 6 ассоциаций
Один-к-одному
Обратите внимание: для того, чтобы решить, в какой из двух таблиц должен быть определен внешний ключ, следует ответить на вопрос о том, какая из таблиц может существовать без другой.
Предположим, что у нас имеется две модели, Foo и Bar. Мы хотим установить между ними отношения один-к-одному таким образом, чтобы Bar содержала атрибут fooId. Это можно реализовать так:
12
Foo.hasOne(Bar);Bar.belongsTo(Foo);
Дальнейший вызов Bar.sync() приведет к отправке в БД следующего запроса:
Возможными вариантами здесь являются: RESTRICT, CASCADE, NO ACTION, SET DEFAULT и SET NULL.
Название внешнего ключа, которое в приведенном примере по умолчанию имеет значение fooId, можно кастомизировать. Причем, это можно делать как в hasOne(), так и в belongsTo():
В случае кастомизации внешнего ключа с помощью объекта, можно определять его тип, значение по умолчанию, ограничения и т.д. Например, в качестве типа внешнего ключа можно использовать DataTypes.UUID вместо дефолтного INTEGER:
По умолчанию ассоциация является опциональной, т.е. fooId может иметь значение null - Bar может существовать без Foo. Для того, чтобы это изменить, можно установить такое ограничение:
12345
Foo.hasOne(Bar,{foreignKey:{allowNull:false,},});
Один-ко-многим
Обратите внимание: в данном случае вопрос о том, в какой из двух таблиц должен быть определен внешний ключ не является актуальным, поскольку такой ключ может быть определен только в целевой модели.
Предположим, что у нас имеется две модели, Team и Player, и мы хотим определить между ними отношения один-ко-многим: в одной команде может быть несколько игроков, но каждый игрок может принадлежат только к одной команде.
12
Team.hasMany(Player);Player.belongsTo(Team);
В данном случае в БД будет отправлен такой запрос:
Обратите внимание: в отличие от первых двух ассоциаций, внешний ключ не может быть определен ни в одной из связанных таблиц. Для этого используется так называемая "соединительная таблица" (junction, join, through table).
Предположим, что у нас имеется две модели, Movie (фильм) и Actor (актер), и мы хотим определить между ними отношения многие-ко-многим: актер может принимать участие в съемках нескольких фильмов, а в фильме может сниматься несколько актеров. Соединительная таблица будет называться ActorMovies и содержать внешние ключи actorId и movieId.
Как упоминалось ранее, Sequelize создает соединительную таблицу автоматически. Но мы вполне можем сделать это самостоятельно:
1 2 3 4 5 6 7 8 9101112131415161718192021222324
constMovie=sequelize.define('Movie',{name:DataTypes.STRING,});constActor=sequelize.define('Actor',{name:DataTypes.STRING,});constActorMovies=sequelize.define('ActorMovies',{MovieId:{type:DataTypes.INTEGER,references:{model:Movie,// или 'Movies'key:'id',},},ActorId:{type:DataTypes.INTEGER,references:{model:Actor,// или 'Actors'key:'id',},},});Movie.belongsToMany(Actor,{through:ActorMovies});Actor.belongsToMany(Movie,{through:ActorMovies});
В этом случае в БД будет отправлен такой запрос:
12345678
CREATETABLEIFNOTEXISTS"ActorMovies"("MovieId"INTEGERNOTNULLREFERENCES"Movies"("id")ONDELETERESTRICTONUPDATECASCADE,"ActorId"INTEGERNOTNULLREFERENCES"Actors"("id")ONDELETERESTRICTONUPDATECASCADE,"createdAt"TIMESTAMPWITHTIMEZONENOTNULL,"updatedAt"TIMESTAMPWITHTIMEZONENOTNULL,UNIQUE("MovieId","ActorId"),-- Note: Sequelize generated this UNIQUE constraint butPRIMARYKEY("MovieId","ActorId")-- it is irrelevant since it's also a PRIMARY KEY);
Выполнение запросов, включающих ассоциации
Предположим, что у нас имеется две модели, Ship (корабль) и Captain (капитан). Между этими моделями существуют отношения один-к-одному. Внешние ключи могут иметь значение null. Это означает, что корабль может существовать без капитана, и наоборот.
"Ленивая" (lazy) или отложенная загрузка позволяет получать ассоциации (т.е. связанные экземпляры) по мере необходимости, а "нетерпеливая" (eager) или немедленная загрузка предполагает получение всех ассоциаций сразу при выполнении запроса.
Пример ленивой загрузки
123456789
constawesomeCaptain=awaitCaptain.findOne({where:{name:'Jack Sparrow',},});// выполняем какие-то операции с капитаном// получаем его корабльconsthisShip=awaitawesomeCaptain.getShip();// выполняем операции с кораблем
В данном случае мы выполняем два запроса - корабль запрашивается при необходимости. Это позволяет сэкономить время и память. Обратите внимание: метод getShip() был создан автоматически, автоматически создаются и другие методы (см. ниже).
Пример нетерпеливой загрузки
1234567
constawesomeCaptaint=awaitCaptain.findOne({where:{name:'Jack Sparrow',},// сразу получаем корабль, которым управляет данный капитанinclude:Ship,});
Создание, обновление и удаление ассоциаций
Для создания, обновления и удаления ассоциаций можно использовать как обычные запросы:
12345
Bar.create({name:'Bar',fooId:3,});// создаем `Bar`, принадлежащую `Foo` c `id` = 3
так и специальные методы/миксины (микшины, примеси, mixins) (см. ниже).
Название внешнего ключа может быть указано при определении ассоциации:
1
Ship.belongsTo(Captain,{foreignKey:'bossId'});
Внешний ключ также может быть определен в виде синонима:
1 2 3 4 5 6 7 8 910
Ship.belongsTo(Captain,{as:'leader'});// будет выброшено исключениеconsole.log((awaitShip.findAll({include:Captain})).toJSON());// следует использовать синонимconsole.log((awaitShip.findAll({include:'leader'})).toJSON());
Синонимы могут использоваться, например, для определения двух разных ассоциаций между одними и теми же моделями. Например, если у нас имеются модели Mail и Person, может потребоваться связать их дважды для представления sender (отправителя) и receiver (получателя) электронной почты. Если мы этого не сделаем, то вызов mail.getPerson() будет двусмысленным. Благодаря синонимам мы получим два метода: mail.getSender() и mail.getReceiver().
При определении синонимов для ассоциаций hasOne() или belongsTo(), следует использовать сингулярную форму (единственное число), а для ассоциаций hasMany() или belongsToMany() - плюральную (множественное число).
Ничто не мешает нам использовать оба способа определения внешних ключей одновременно:
При определении ассоциации между двумя моделями, экземпляры этих моделей получают специальные методы для взаимодействия с другой частью ассоциации. Конкретные методы зависят от типа ассоциации. Предположим, что у нас имеется две связанные модели, Foo и Bar.
Foo.hasOne(Bar)
foo.getBar()
foo.setBar()
foo.createBar()
1 2 3 4 5 6 7 8 91011121314
constfoo=awaitFoo.create({name:'foo'});constbar=awaitBar.create({name:'bar'});constbar2=awaitBar.create({name:'another bar'});console.log(awaitfoo.getBar());// nullawaitfoo.setBar(bar);console.log(awaitfoo.getBar().name);// barawaitfoo.createBar({name:'and another bar'});console.log(awaitfoo.getBar().name);// and another barawaitfoo.setBar(null);// удаляем ассоциациюconsole.log(awaitfoo.getBar());// null
Для формирования названий методов вместо названия модели может использоваться синоним, например:
1
Task.hasOne(User,{as:'Author'});
task.getAuthor()
task.setAuthor()
task.createAuthor()
Обратите внимание: как было отмечено ранее, ассоциации определяются в паре. Это объясняется тем, что обе модели должны знать о существовании ассоциации между ними.
допустим, что мы определили только ассоциацию Foo.hasOne(Bar)
12345
// это будет работатьawaitFoo.findOne({include:Bar});// а здесь будет выброшено исключениеawaitBar.findOne({include:Foo});
если мы определим пару ассоциаций, то все будет в порядке
1234567
Bar.belongsTo(Foo);// работаетawaitFoo.findOne({include:Bar});// и это тожеawaitBar.findOne({include:Foo});
Синонимы позволяют определять несколько ассоциаций между одними и теми же моделями:
Создание ассоциаций с помощью полей, которые не являются первичными ключами
В качестве внешних ключей могут использоваться не только основные ключи, но и другие поля. Единственным требованием к полю, используемому в качестве внешнего ключа, является то, что его значение должно быть уникальным, в противном случае, это не будет иметь смысла.
belongsTo()
Ассоциация A.belongsTo(B) приводит к созданию внешнего ключа в модели-источнике (A).
Снова вернемся к примеру с кораблями и ограничим уникальность имен капитанов:
Теперь в качестве внешнего ключа вместо captainId можно использовать captainName. Для этого в ассоциации необходимо определить настройки targetKey и foreignKey:
constFoo=sequelize.define('foo',{name:{type:DataTypes.STRING,unique:true,},},{timestamps:false,});constBar=sequelize.define('bar',{title:{type:DataTypes.STRING,unique:true,},},{timestamps:false,});constBaz=sequelize.define('baz',{summary:DataTypes.STRING,},{timestamps:false,});Foo.hasOne(Bar,{sourceKey:'name',foreignKey:'fooName',});Bar.hasMany(Baz,{sourceKey:'title',foreignKey:'barTitle',});awaitBar.setFoo('Название для `Foo`');awaitBar.addBar('Название для `Bar`');
belongsToMany()
В данном случае необходимо определить два внешних ключа в соединительной таблице.
A.belongsTo(B) - внешний ключ хранится в модели-источнике (A), ссылка (targetKey) - в целевой модели (B)
A.hasOne(B) и A.hasMany(B) - внешний ключ хранится в целевой модели (B), а ссылка (sourceKey) - в источнике (A)
A.belongsToMany(B) - используется соединительная таблица, в которой хранятся ключи для sourceKey и targetKey, sourceKey соответствует некоторому полю в источнике (A), targetKey - некоторому полю в целевой модели (B)
Sequelize поддерживает создание так называемых "параноидальных" (paranoid) таблиц. Из таких таблиц данные по-настоящему не удаляются. Вместо этого, в них добавляется колонка deletedAt в момент выполнения запроса на удаление. Это означает, что в таких таблицах выполняется мягкое удаление (soft-deletion).
Для создания параноика используется настройка paranoid: true. Обратите внимание: для работы такой таблицы требуется фиксация времени создания и обновления таблицы. Поэтому для них нельзя устанавливать timestamps: false. Название поля deletedAt можно кастомизировать.
При вызове метода destroy() производится мягкое удаление:
12345
awaitPost.destroy({where:{id:1,},});// UPDATE "posts" SET "deletedAt"=[timestamp] WHERE "deletedAt" IS NULL AND "id" = 1;
Для окончательного удаления параноика следует использовать настройку force: true:
123456
awaitPost.destroy({where:{id:1,},force:true,});
Для восстановления "удаленного" значения используется метод restore():
1 2 3 4 5 6 7 8 91011121314
constpost=awaitPost.create({title:'test'});awaitpost.destroy();console.log('Пост удален мягко!');awaitpost.restore();console.log('Пост восстановлен!');// восстанавливаем "удаленные" посты, набравшие больше 100 лайков, с помощью статического метода `restore()`awaitPost.restore({where:{likes:{[Op.gt]:100,},},});
По умолчанию запросы, выполняемые Sequelize, будут игнорировать "удаленные" записи. Это означает, что метод findAll(), например, вернет только "неудаленные" записи, а метод findByPk() при передаче ему первичного ключа "удаленной" записи, вернет null.
Для учета "удаленных" записей при выполнении запроса используется настройка paranoid: false:
Настройка required позволяет фильтровать результат выполняемого запроса - конвертировать OUTER JOIN в INNER JOIN. В следующем примере возвращаются только те пользователи, у которых есть задачи:
Фильтрацию на уровне связанной модели можно выполнять с помощью настройки where. В следующем примере возвращаются только пользователи, у которых имеется хотя бы один инструмент НЕ маленького размера:
При этом, уровень вложенности фильтруемых колонок значения не имеет.
Для лучшего понимания разницы между использование внутреннего where (в include) с настройкой required и без нее, а также использованием where на верхнем уровне с помощью синтаксиса $nested.column$, рассмотрим 4 примера:
// внутренний `where` с `required: true` по умолчаниюawaitUser.findAll({include:{model:Tool,as:'Instruments',where:{size:{[Op.ne]:'small',},},},});// внутренний `where` с `required: false`awaitUser.findAll({include:{model:Tool,as:'Instruments',where:{size:{[Op.ne]:'small',},required:false,},},});// использование `where` на верхнем уровне с `required: false`awaitUser.findAll({where:{'$Instruments.size$':{[Op.ne]:'small',},},include:{model:Tool,as:'Instruments',},});// использование `where` на верхнем уровне с `required: true`awaitUser.findAll({where:{'$Instruments.size$':{[Op.ne]:'small',},},include:{model:Tool,as:'Instruments',required:true,},});
Для включения всех связанных моделей используются настройки all и nested:
12345
// получаем все модели, связанные с `User`User.findAll({include:{all:true}});// получаем все модели, связанные с `User`, вместе со связанными с ними моделямиUser.findAll({include:{all:true,nested:true}});
Сортировка связанных экземпляров при нетерпеливой загрузке
Для сортировки связанных экземпляров используется настройка order (на верхнем уровне):
Company.findAll({include:Division,order:[// массив для сортировки начинается с модели// затем следует название поля и порядок сортировки[Division,'name','ASC'],],});Company.findAll({include:Division,order:[[Division,'name','DESC']],});Company.findAll({// с помощью синонимаinclude:{model:Division,as:'Div'},order:[[{model:Division,as:'Div'},'name','DESC'],],});Company.findAll({// несколько уровней вложенностиinclude:{model:Division,include:Department,},order:[[Division,Department,'name','DESC']],});
В случае с отношениями многие-ко-многим, у нас имеется возможность выполнять сортировку по атрибутам соединительной таблицы. Предположим, что между моделями Division и Department существуют такие отношения, а соединительная таблица между ними называется DepartmentDivision:
Данный запрос выполняет внешнее соединение (OUTER JOIN). Применение настройки where к связанной модели произведет внутреннее соединение (INNER JOIN) - будут возвращены только экземпляры, которые имеют совпадающие подмодели. Для получения всех родительских экземпляров используется настройка required: false:
1 2 3 4 5 6 7 8 91011121314151617
User.findAll({include:[{model:Tool,as:'Instruments',include:[{model:Teacher,where:{school:'Woodstock Music School',},required:false,},],},],});
Данный запрос вернет всех пользователей и их инструменты, но только тех учителей, которые связаны с Woodstock Music School.
Утилита findAndCountAll() поддерживает include. При этом, только модели, помеченные как required, будут учитываться count.
В данном случае мы получим 3 пользователей, у которых есть профили. Если мы опустим required, то получим 3 пользователей, независимо от того, имеется у них профиль или нет. Включение where в include автоматически делает его обязательным.
Экземпляр может быть создан сразу с вложенной ассоциацией. Однако, выполнение обновлений и удалений вложенных объектов в настоящее время не поддерживается.
belongsTo(), hasMany(), hasOne()
Рассмотрим пример:
1 2 3 4 5 6 7 8 9101112131415161718
constProduct=sequelize.define('product',{title:DataTypes.STRING,});constUser=sequelize.define('user',{firstName:DataTypes.STRING,lastName:DataTypes.STRING,});constAddress=sequelize.define('address',{type:DataTypes.STRING,line1:DataTypes.STRING,line2:DataTypes.STRING,city:DataTypes.STRING,state:DataTypes.STRING,zip:DataTypes.STRING,});// сохраняем значения, возвращаемые при создании ассоциации для дальнейшего использованияProduct.User=Product.belongsTo(User);User.Address=User.hasMany(Address);
Новый Product, User и один или более Address могут быть созданы одновременно:
Обратите внимание: наша модель называется user с маленькой буквы u. Это означает, что свойство объекта также должно называться user. Если бы мы определили модель как User, то для соответствующего свойства нужно было бы использовать User. То же самое касается addresses, учитывая плюрализацию (перевод во множетвенное число).
Последний пример может быть расширен для поддержки ассоциаций:
Передавая строку в through, мы просим Sequelize автоматически создать модель - соединительную таблицу User_Profiles, содержащую 2 колонки: userId и profileId. В эти колонки будут записываться уникальные композиционные (unique composite) ключи.
Определение соединительной таблицы самостоятельно имеет некоторые преимущества по сравнению с ее автоматической генерацией. Мы, например, можем определять дополнительные атрибуты в такой таблице:
После этого мы можем получать дооплнительную информацию из соединительной таблицы. Например, при вызове user.addProfile() мы можем передавать значения для дополнительной колонки с помощью настройки through:
Вероятно, вы заметили, что в таблице User_Profiles отсутствует поле id. Дело в том, что такая таблица содержит уникальный композиционный ключ, название которого генерируется автоматически, но это можно изменить с помощью настройки uniqueKey:
Что если вместо определения отношения многие-ко-многим мы сделаем так?
1234567
// определяем отношение один-ко-многим между `User` и `Grant`User.hasMany(Grant);Grant.belongsTo(User);// определяем отношение один-ко-многим между `Profile` и `Grant`Profile.hasMany(Grant);Grant.belongsTo(Profile);
Результат будет таким же! Это объясняется тем, что User.hasMany(Grant) и Profile.hasMany(Grant) запишут userId и profileId в Grant.
Это показывает, что отношения многие-ко-многим не сильно отличаются от двух ассоциаций один-ко-многим. Единственным отличием между ними является то, как будет работать нетерпеливая загрузка:
// многие-ко-многим позволяет делать такUser.findAll({include:Profile});Profile.findAll({include:User});// но не такUser.findAll({include:Grant});Profile.findAll({include:Grant});Grant.findAll({include:User});Grant.findAll({include:Profile});// с другой стороны, пара ассоциаций один-ко-многим позволяет делать следующееUser.findAll({include:Grant});Profile.findAll({include:Grant});Grant.findAll({include:User});Grant.findAll({include:Profile});// но не такUser.findAll({include:Profile});Profile.findAll({include:User});// хотя мы можем имитировать нечто похожееUser.findAll({include:{model:Grant,include:Profile,},});/* Это похоже на `User.findAll({ include: Profile })`, но структура результирующего объекта будет немного другой. Вместо `user.profiles[].grant` мы получим `user.grants[].profiles[]`*/
На самом деле, для успешного выполнения всех указанных выше операций достаточно скомбинировать оба подхода:
1 2 3 4 5 6 7 8 91011121314
User.belongsToMany(Profile,{through:Grant});Profile.belongsToMany(User,{through:Grant});User.hasMany(Grant);Grant.belongsTo(User);Profile.hasMany(Grant);Grant.belongsTo(Profile);// все работаетUser.findAll({include:Profile});Profile.findAll({include:User});User.findAll({include:Grant});Profile.findAll({include:Grant});Grant.findAll({include:User});Grant.findAll({include:Profile});
Это позволяет выполнять все виды вложенных включений:
В случае с ассоциацией многие-ко-многим синонимы определяются следующим образом:
1 2 3 4 5 6 7 8 910111213141516171819202122
Product.belongsToMany(Category,{as:'groups',through:'product_categories',});Category.belongsToMany(Product,{as:'items',through:'product_categories',});// НЕ работаетawaitProduct.findAll({include:Category});// работаетawaitProduct.findAll({include:{model:Category,as:'groups',},});// это тоже работаетawaitProduct.findAll({include:'groups'});
Вот как выглядит SQL-запрос на создание таблицы product_categories:
Как мы видим, внешними ключами являются productId и categoryId. Для изменения этих названий используются настройки foreignKey и otherKey, соответственно (foreignKey определяет ключ для модели-источника, а otherKey - для целевой модели).
Обратите внимание: настройки foreignKey и otherKey должны определяться в обоих вызовах. Если определить их только в одном вызове, поведение Sequelize будет непредсказуемым.
Sequelize также поддерживает циклические отношения многие-ко-многим:
12345
Person.belongsToMany(Person,{as:'Children',through:'PersonChildren',});// это создаст таблицу `PersonChildren` с идентификаторами объектов
Определение возвращаемых атрибутов соединительной таблицы
По умолчанию при нетерпеливой загрузке в случае с ассоциацией многие-ко-многим возвращается такой объект (User.findOne({ include: Profile })):
Для исключения поля grant из результатов запроса можно указать attributes: [].
При использовании миксинов (например, user.getProfiles()), вместо методов для поиска (например, User.findAll()), для фильтрации полей соединительной таблицы используется настройка joinTableAttributes:
Предположим, что мы моделируем игру. У нас есть игроки и команды. Команды играют в игры. Игроки могут менять команды в середине чемпионата (но не в середине игры). В одной игре участвует две команды, в каждой команде имеется свой набор игроков (для текущей игры).
Вопрос: как определить ассоциацию между этими моделями?
Первое, что можно заметить:
одна игра имеет несколько связанных с ней команд (тех, которые играют в этой игре)
одна команда может принимать участие в нескольких играх
Это означает, что между моделями Game и Team должны существовать отношения многие-ко-многим. Реализуем супер-вариант названной ассоциации (как в предыдущем примере):
С игроками все несколько сложнее. Набор игроков, формирующих команду, зависит не только от команды, но также от того, в какой игре данная команда участвует. Поэтому нам не нужна ассоциация многие-ко-многим между Player и Team. Нам также не нужна ассоциация многие-ко-многим между Player и Game. Вместо привязки Player к одной из указанных моделей, нам требуется ассоциация между Player и чем-то вроде "парного ограничения команда-игра", поскольку именно пара (команда + игра) определяет набор игроков. Внезапно, то, что мы искали, оказывается соединительной таблицей GameTeam! Учитывая, что конкретная пара команда-игра определяет несколько игроков и один игрок может участвовать в нескольких парах, нам требуется ассоциация многие-ко-многим между Player и GameTeam.
Для обеспечения максимальной гибкости снова прибегнем к супер-версии M:N:
const{Sequelize,Op,Model,DataTypes,}=require('sequelize');constsequelize=newSequelize('sqlite::memory:',{define:{timestamps:false},// Просто, чтобы не повторяться});constPlayer=sequelize.define('Player',{username:DataTypes.STRING,});constTeam=sequelize.define('Team',{name:DataTypes.STRING,});constGame=sequelize.define('Game',{name:DataTypes.INTEGER,});// Ассоциация супер-многие-ко-многим между `Game` и `Team`constGameTeam=sequelize.define('GameTeam',{id:{type:DataTypes.INTEGER,primaryKey:true,autoIncrement:true,allowNull:false,},});Team.belongsToMany(Game,{through:GameTeam});Game.belongsToMany(Team,{through:GameTeam});GameTeam.belongsTo(Game);GameTeam.belongsTo(Team);Game.hasMany(GameTeam);Team.hasMany(GameTeam);// Ассоциация супер-многие-ко-многим между `Player` и `GameTeam`constPlayerGameTeam=sequelize.define('PlayerGameTeam',{id:{type:DataTypes.INTEGER,primaryKey:true,autoIncrement:true,allowNull:false,},});Player.belongsToMany(GameTeam,{through:PlayerGameTeam});GameTeam.belongsToMany(Player,{through:PlayerGameTeam});PlayerGameTeam.belongsTo(Player);PlayerGameTeam.belongsTo(GameTeam);Player.hasMany(PlayerGameTeam);GameTeam.hasMany(PlayerGameTeam);(async()=>{awaitsequelize.sync();// Создаем игроковawaitPlayer.bulkCreate([{username:'s0me0ne'},{username:'empty'},{username:'greenhead'},{username:'not_spock'},{username:'bowl_of_petunias'},]);// Создаем игрыawaitGame.bulkCreate([{name:'The Big Clash'},{name:'Winter Showdown'},{name:'Summer Beatdown'},]);// Создаем командыawaitTeam.bulkCreate([{name:'The Martians'},{name:'The Earthlings'},{name:'The Plutonians'},]);// Начнем с определения того, какая команда в какой игре участвует. Это можно сделать// несколькими способами, например, посредством вызова `setTeams()` для каждой игры.// Однако, для чистоты эксперимента, мы используем явные вызовы `create()`awaitGameTeam.bulkCreate([{GameId:1,TeamId:1},// эта `GameTeam` получит `id` 1{GameId:1,TeamId:2},// и т.д.{GameId:2,TeamId:1},{GameId:2,TeamId:3},{GameId:3,TeamId:2},{GameId:3,TeamId:3},]);// Теперь определим игроков.// Сделаем это только для второй игры (Winter Showdown).awaitPlayerGameTeam.bulkCreate([{PlayerId:1,GameTeamId:3},// s0me0ne играет за The Martians{PlayerId:3,GameTeamId:3},// и т.д.{PlayerId:4,GameTeamId:4},{PlayerId:5,GameTeamId:4},]);// После этого мы можем выполнять запросы!constgame=awaitGame.findOne({where:{name:'Winter Showdown',},include:{model:GameTeam,include:[{model:Player,through:{attributes:[]},// Скрываем нежелательные вложенные объекты `PlayerGameTeam` из результатов},Team,],},});console.log(`Обнаружена игра: "${game.name}"`);for(leti=0;i<game.GameTeams.length;i++){constteam=game.GameTeams[i].Team;constplayers=game.GameTeams[i].Players;console.log(`- Команда "${team.name}" играет в игру "${game.name}" со следующими игроками:`);console.log(players.map((p)=>`--- ${p.username}`).join('\n'));}})();
Вывод:
1234567
Обнаружена игра: "Winter Showdown"
- Команда "The Martians" играет в игру "Winter Showdown" со следующими игроками:
--- s0me0ne
--- greenhead
- Команда "The Plutonians" играет в игру "Winter Showdown" со следующими игроками:
--- not_spock
--- bowl_of_petunias
Область видимости ассоциаций (assosiation scopes) похожа на области видимости моделей в том, что обе автоматически применяют к запросам такие вещи, как предложение where; разница между ними состоит в том, что область модели применяется к вызовам статических методов для поиска, а область ассоциации - к вызовам поисковых методов экземпляра (таким как миксины).
Пример применения области ассоциации для отношений один-ко-многим:
Полиморфная ассоциация (polymorphic assosiation) состоит из двух и более ассоциаций, взаимодействующих с одним внешним ключом.
Предположим, что у нас имеется три модели: Image, Video и Comment. Первые две модели - это то, что может разместить пользователь. Мы хотим разрешить комментирование этих вещей. На первый вгляд, может показаться, что требуются такие ассоциации:
ассоциация один-ко-многим между Image и Comment
12
Image.hasMany(Comment);Comment.belongsTo(Image);
ассоциация один-ко-многим между Video и Comment
12
Video.hasMany(Comment);Comment.belongsTo(Video);
Однако, это может привести к тому, что Sequelize создаст в таблице Comment два внешних ключа: imageId и videoId. Такая структура означает, что комментарий добавляется одновременно к одному изображению и одному видео, что не соответствует действительности. Нам нужно, чтобы Comment указывал на единичный Commentable, абстрактную полиморфную сущность, представляющую либо Image, либо Video.
Перед настройкой такой ассоциации, рассмотрим пример ее использования:
1 2 3 4 5 6 7 8 910111213141516
constimage=awaitImage.create({url:'http://example.com',});constcomment=awaitimage.createComment({content:'Круто!',});copnsole.log(comment.commentableId===image.id);// true// Мы можем получать информацию о том, с каким типом `commentable` связан комментарийconsole.log(comment.commentableType);// Image// Мы можем использовать полиморфный метод для извлечения связанного `commentable`,// независимо от того, чем он является, `Image` или `Video`constassociatedCommentable=awaitcomment.getCommentable();// Обратите внимание: `associatedCommentable` - это не то же самое, что `image`
Создание полиассоциации один-ко-многим
Для настройки полиассоциации для приведенного выше примера (полиассоциации один-ко-многим) необходимо выполнить следующие шаги:
определить строковое поле commentableType в модели Comment
определить ассоциацию hasMany и belongsTo между Image / Video и Comment
отключить ограничения ({ constraints: false }), поскольку один и тот же внешний ключ будет ссылаться на несколько таблиц
определить соответствущую область видимости ассоциации
для поддержки ленивой загрузки - определить новый метод экземпляра getCommentable() в модели Comment, который под капотом будет вызывать правильный миксин для получения соответствующего commentable
для поддержки нетерпеливой загрузки - определить хук afterFind() в модели Comment, автоматически заполняющий поле commentable каждого экземпляра
для предотвращения ошибок при нетерпеливой загрузке, можно удалять поля image и video из экземпляров комментария в хуке afterFind(), оставляя в них только абстрактное поле commentable
// Вспомогательная функцияconstcapitilize=([first,...rest])=>`${first.toUpperCase()}${rest.join('').toLowerCase()}`;constImage=sequelize.define('image',{title:DataTypes.STRING,url:DataTypes.STRING,});constVideo=sequelize.define('video',{title:DataTypes.STRING,text:DataTypes.STRING,});// в данном случае нам необходимо создать статическое поле, поэтому мы используем расширение `Model`classCommentextendsModel{getCommentable(options){if(!this.commentableType)returnPromise.resolve(null);constmixinMethodName=`get${capitilize(this.commentableType)}`;returnthis[mixinMethodName](options);}}Comment.init({title:DataTypes.STRING,commentableId:DataTypes.INTEGER,commentableType:DataTypes.STRING,},{sequelize,modelName:'comment'});Image.hasMany(Comment,{foreignKey:'commentableId',constraints:false,scope:{commentableType:'image',},});Comment.belongsTo(Image,{foreignKey:'commentableId',constraints:false,});Video.hasMany(Comment,{foreignKey:'commentableId',constraints:false,scope:{commentableType:'video',},});Comment.belongsTo(Video,{foreignKey:'commentableId',constraints:false,});Comment.addHook('afterFind',(findResult)=>{if(!Array.isArray(findResult))findResult=[findResult];for(constinstanceoffindResult){if(instance.commentableType==='image'&&instance.image!==undefined){instance.commentable=instance.image;}elseif(instance.commentableType==='video'&&instance.video!==undefined){instance.commentable=instance.video;}// Для предотвращения ошибокdeleteinstance.image;deleteinctance.dataValues.image;deleteinstance.video;deleteinstance.dataValues.video;}});
Поскольку колонка commentableId ссылается на несколько таблиц (в данном случае две), мы не можем применить к ней ограничение REFERENCES. Поэтому мы указываем constraints: false.
Обратите внимание на следующее:
ассоциация Image -> Comment определяет область { commentableType: 'image' }
ассоциация Video -> Comment определяет область { commentableType: 'video' }
Эти области автоматически применяются при использовании ассоциативных функций. Несколько примеров:
Метод экземпляра getCommentable() предоставляет абстракцию для ленивой загрузки связанного commentable - комментария, принадлежащего Image или Video.
Это работает благодаря преобразованию строки commentableType в вызов правильного миксина (getImage() или getVideos(), соответственно).
Обратите внимание, что приведенная выше реализация getCommentable():
возвращает null при отсутствии ассоциации
позволяет передавать объект с настройками в getCommentable(options), подобно любому другому (стандартному) методу. Это может пригодиться, например, при определении условий или включений.
Полиморфная нетерпеливая загрузка
Теперь мы хотим выполнить полиморфную нетерпеливую загрузку связанных commentable для одного (или более) комментария:
123456
constcomment=awaitComment.findOne({include:[/* Что сюда поместить? */],});console.log(comment.commentable);// Наша цель
Решение состоит во включении Image и Video для того, чтобы хук afterFind() мог автоматически добавить поле commentable в экземпляр.
Например:
1234567
constcomments=awaitComment.findAll({include:[Image,Video],});for(constcommentofcomments){constmessage=`Найден комментарий #${comment.id} с типом '${comment.commentableType}':\n`;console.log(message,comment.commentable.toJSON());}
Предположим, что вместо комментариев у нас имеются теги. Соответственно, вместо commentables у нас будут taggables. Один taggable может иметь несколько тегов, в то же время один тег может быть помещен в несколько taggable.
Для настройки рассматриваемой полиассоциации необходимо выполнить следующие шаги:
явно создать соединительную модель, определив в ней два внешних ключа: tagId и taggableId (данная таблица будет соединять Tag и taggable)
определить в соединительной таблице строковое поле taggableType
определить ассоциацию belongsToMany() между двумя моделями и Tag:
отключить ограничения ({ constraints: false }), поскольку один и тот же внешний ключ будет ссылаться на несколько таблиц
определить соответствующие области видимости ассоциаций
определить новый метод экземпляра getTaggables() в модели Tag, который под капотом будет вызывать правильный миксин для получения соответствующих taggables
classTagextendsModel{getTaggables(options){constimages=awaitthis.getImages(options)constvideos=this.getVideos(options)// Объединяем изображения и видео в один массив `taggables`returnimages.concat(videos)}}Tag.init({name:DataTypes.STRING},{sequelize,moelName:'tag'})// Явно определяем соединительную таблицуconstTag_Taggable=sequelize.define('tag_taggable',{tagId:{type:DataTypes.INTEGER,unique:'tt_unique_constraint'},taggableId:{type:DataTypes.INTEGER,unique:'tt_unique_contraint'},taggableType:{type:DataTypes.STRING,unique:'tt_unique_constraint'}})Image.belongsToMany(Tag,{through:{model:Tag_Taggable,unique:false,scope:{taggableType:'image'}},foreignKey:'taggableId',constraints:false})Tag.belongsToMany(Image,{through:{model:Tag_Taggable,unique:false,foreignKey:'tagId',constraints:false}})Video.belongsToMany(Tag,{through:{model:Tag_Taggable,unique:false,scope:{taggableType'video'}},foreignKey:'taggableId',constraints:false})Tag.belongsToMany(Video,{through:{model:Tag_Taggable,unique:false},foreignKey:'tagId',constraints:false})
Область ассоциации может применяться не только к соединительной таблице, но и к целевой модели.
Добавим тегам статус. Для получения всех тегов со статусом pending определим еще одну ассоциацию belongsToMany() между Image и Tag, применив область ассоциации как к соединительной таблице, так и к целевой модели:
Sequelize поддерживает выполнение двух видов транзакций:
Неуправляемые (unmanaged): завершение транзакции и отмена изменений выполняются вручную (путем вызова соответствующих методов)
Управляемые (managed): при возникновении ошибки изменения автоматически отменяются, а при успехе транзакции автоматически выполняется фиксация (commit) изменений
// Сначала мы запускаем транзакцию и сохраняем ее в переменнуюconstt=awaitsequelize.transaction();try{// Затем при выполнении операций передаем транзакцию в качестве соответствующей настройкиconstuser=awaitUser.create({firstName:'John',lastName:'Smith',},{transaction:t});awaituser.addSibling({firstName:'Jane',lastName:'Air',},{transaction:t});// Если выполнение кода достигло этой точки,// значит, выполнение операций завершилось успешно -// фиксируем измененияawaitt.commit();}catch(err){// Если выполнение кода достигло этой точки,// значит, во время выполнения операций возникла ошибка -// отменяем измененияawaitt.rollback();}
Управляемые тразакции
Для выполнения управляемой транзакции в sequelize.transaction() передается функция обратного вызова. Далее происходит следующее:
Sequelize автоматически запускает транзакцию и создает объект t
Затем выполняется переданный колбек, которому передается t
При возникновении ошибки, изменения автоматически отменяются
При успехе транзакции, изменения автоматически фиксируются
Таким образом, sequelize.transaction() либо разрешается с результатом, возвращаемым колбеком, либо отклоняется с ошибкой.
try{constresult=awaitsequelize.transaction(async(t)=>{constuser=awaitUser.create({firstName:'John',lastName:'Smith',},{transaction:t});awaituser.addSibling({firstName:'Jane',lastName:'Air',},{transaction:t});returnuser;});// Если выполнение кода достигло этой точки,// значит, выполнение операций завершилось успешно -// фиксируем изменения}catch(err){// Если выполнение кода достигло этой точки,// значит, во время выполнения операций возникла ошибка -// отменяем изменения}
Обратите внимание: при выполнении управляемой транзакции, нельзя вручную вызывать методы commit() и rollback().
Автоматическая передача транзакции во все запросы
В приведенных примерах транзакция передавалась вручную - { transaction: t }. Для автоматической передачи транзакции во все запросы необходимо установить модуль cls-hooked (CLS - Continuation Local Storage, "длящееся" локальное хранилище) и инстанцировать пространство имен (namespace):
Обратите внимание: мы вызываем метод useCLS() на конструкторе, а не на экземпляре. Это означает, что пространство имен будет доступно всем экземплярам, а также, что CLS - это "все или ничего", нельзя включить его только для некоторых экземпляров.
CLS представляет собой что-то вроде локального хранилища в виде потока для колбеков. На практике это означает, что разные цепочки из колбеков могут использовать локальные переменные из одного пространства CLS. После включения CLS, t автоматически передается при создании транзакции. Поскольку переменные являются частными для цепочки колбеков, одновременно может выполняться несколько транзакций:
В большинстве случаев, в явном вызове namespace.get('transaction') нет необходимости, поскольку все запросы автоматически получают транзакцию из пространства имен:
1234
sequelize.transaction((t1)=>{// С включенным CLS пользователь будет создан внутри транзакцииreturnUser.create({name:'John'});});
Параллельные/частичные транзакции
С помощью последовательности запросов можно выполнять параллельные транзакции. Также имеется возможность исключать запросы из транзакции. Для управления тем, каким транзакциям принадлежит запрос, используется настройка transaction (обратите внимание: SQLite не поддерживает одновременное выполнение более одной транзакции).
С включенным CLS:
1 2 3 4 5 6 7 8 91011121314151617
sequelize.transaction((t1)=>{returnsequelize.transaction((t2)=>{// С включенным `CLS` все запросы здесь по умолчанию будут использовать `t2`// Настройка `transaction` позволяет это изменитьreturnPromise.all([User.create({name:'John'},{transaction:null}),User.create({name:'Jane'},{transaction:t1}),User.create({name:'Alice'}),// этот запрос будет использовать `t2`]);});});
Объект transaction позволяет регистрировать фиксацию изменений. Хук afterCommit() может быть добавлен как к управляемым, так и к неуправляемым объектам транзакции:
колбек, передаваемый в afterCommit(), является асинхронным. В данном случае:
для управляемой транзакции: вызов sequelize.transaction() будет ждать его завершения
для неуправляемой транзакции: вызов t.commit() будет ждать его завершения
Обратите внимание на следующее:
afterCommit() не запускается при отмене изменений
он не модифицирует значение, возвращаемое транзакцией (в отличие от других хуков)
Хук afterCommit() можно использовать в дополнение к хукам модели для определения момента сохранения экземпляра и его доступности за пределами транзакции:
1234567
User.afterSave((instance,options)=>{if(options.transaction){// Ожидаем фиксации изменений для уведомления подписчиков о сохранении экземпляраoptions.transaction.afterCommit(()=>/* Уведомление */)return}})
Хуки или события жизненного цикла (hooks) - это функции, которые вызываются до или после вызова методов Sequelize. Например, для установки значения модели перед ее сохранением можно использовать хук beforeUpdate().
Обратите внимание: хуки могут использоваться только на уровне моделей.
Доступные хуки
Sequelize предоставляет большое количество хуков. Их полный список можно найти здесь. Порядок вызова наиболее распространенных хуков следующий:
Аргументы в хуки передаются по ссылкам. Это означает, что мы можем модифицировать значения и это отразится на соответствующих инструкциях. Хук может содержать асинхронные операции - в этом случае функция должна возвращать промис.
Существует три способа программного добавления хуков:
// 1) через метод `init()`classUserextendsModel{}User.init({username:DataTypes.STRING,mood:{type:DataTypes.ENUM,values:['счастливый','печальный','индифферентный',],},},{hooks:{beforeValidate:(user,options)=>{user.mood='счастливый';},afterValidate:(user,options)=>{user.username='Ванька';},},sequelize,});// 2) через метод `addHook()`User.addHook('beforeValidate',(user,options)=>{user.mood='счастливый';});User.addHook('afterValidate','someCustomName',(user,options)=>{returnPromise.reject(newError('К сожалению, я не могу позволить вам этого сделать.'));});// 3) напрямуюUser.beforeCreate(async(user,options)=>{consthashedPassword=awaithashPassword(user.password);user.password=hashedPassword;});User.afterValidate('myAfterHook',(user,options)=>{user.username='Ванька';});
Обратите внимание, что удаляться могут только именованные хуки:
Глобальными называются хуки, которые выполняются для всех моделей. Особенно полезными такие хуки являются в плагинах. Они определяются двумя способами:
в настройках конструктора (хуки по умолчанию)
1 2 3 4 5 6 7 8 910111213141516171819202122
constsequelize=newSequelize(/*...*/,{define:{hooks:{beforeCreate(){// ...}}}})// Дефолтные хуки запукаются при отсутствии в модели аналогичных хуковconstUser=sequelize.define('user',{})constProject=sequelize.define('project',{},{hooks:{beforeCreate(){// ...}}})awaitUser.create({})// запускается глобальный хукawaitProject.create({})// запускается локальный хук
с помощью sequelize.addHook() (постоянные хуки)
1 2 3 4 5 6 7 8 91011121314151617181920
sequelize.addHook('beforeCreate',()=>{// ...});// Такой хук запускается независимо от наличия у модели аналогичного хукаconstUser=sequelize.define('user',{});constProject=sequelize.define('project',{},{hooks:{beforeCreate(){// ...},},});awaitUser.create({});// запускается глобальный хукawaitProject.create({});// сначала запускается локальный хук, затем глобальный
Хуки, связанные с подключением к БД
Существует 4 хука, выполняемые до и после подключения к БД и отключения от нее:
sequelize.beforeConnect(callback) - колбек имеет сигнатуру async (config) => {}
Эти хуки могут использоваться для асинхронного получения полномочий (credentials) для доступа к БД или получения прямого доступа к низкоуровневому соединению с БД после его установки.
Например, мы можем асинхронно получить пароль от БД из хранилища токенов и модифицировать объект с настройками:
User.beforeCreate((user)=>{if(user.accessLevel>10&&user.username!=='Сенсей'){thrownewError('Вы не можете предоставить этому пользователю уровень доступа выше 10');}});// Будет выброшено исключениеtry{awaitUser.create({username:'Гуру',accessLevel:20,});}catch(err){console.error(err);// Вы не можете предоставить этому пользователю уровень доступа выше 10}// Окconstuser=awaitUser.create({username:'Сенсей',accessLevel:20,});
Хуки моделей
При вызове методов bulkCreate(), update() и destroy() запускаются следующие хуки:
beforeBulkCreate(callback) - колбек имеет сигнатуру (instances, options) => {}
Обратите внимание: вызов методов моделей по умолчанию приводит к запуску только хуков с префиксом bulk. Это можно изменить с помощью настройки { individualHooks: true }, но имейте ввиду, что это может крайне негативно сказаться на производительности.
Однако, добавление hooks: true отключает оптимизации. В этом случае Sequelize сначала выполняет выборку связанных объектов с помощью SELECT и затем уничтожает каждый экземпляр по одному для обеспечения вызова соответствующих хуков с правильными параметрами.
Многие-ко-многим
при использовании миксинов add для отношений belongsToMany() (когда в соединительной таблице создается как минимум одна запись) запускаются хуки beforeBulkCreate() и afterBulkCreate() соединительной таблицы
если указано { individualHooks: true }, то также вызываются индивидуальные хуки
при использовании миксинов remove запускаются хуки beforeBulkDestroy() и afterBulkDestroy(), а также индивидуальные хуки при наличии { individualHooks: true }
Хуки и транзакции
Если в оригинальном вызове была определена транзакция, она будет передана в хук вместе с другими настройками:
1 2 3 4 5 6 7 8 9101112131415161718192021
User.addHook('afterCreate',async(user,options)=>{// Мы можем использовать `options.transaction` для выполнения другого вызова// с помощью той же транзакции, которая запустила данный хукawaitUser.update({mood:'печальный'},{where:{id:user.id,},transaction:options.transaction,});});awaitsequelize.transaction(async(t)=>{awaitUser.create({username:'Ванька',mood:'счастливый',transaction:t,});});
Если мы не передадим транзакцию в вызов User.update(), обновления не произойдет, поскольку созданный пользователь попадет в БД только после фиксации транзакции.
Важно понимать, что Sequelize автоматически использует транзакции при выполнении некоторых операций, таких как Model.findOrCreate(). Если хуки выполняют операции чтения или записи на основе объекта из БД или модифицируют значения объекта как в приведенном выше примере, всегда следует определять { transaction: options.transaction }.
Каждый экземпляр использует интерфейс запросов (query interface) для взаимодействия с БД. Методы этого интерфейса являются низкоуровневыми в сравнении с обычными методами. Но, разумеется, по сравнению с запросами SQL, они являются высокоуровневыми.
Рассмотрим несколько примеров использования методов названного интерфейса (полный список методов можно найти здесь).
Sequelize предоставляет настройку underscored для моделей. Когда эта настройка имеет значение true, значение настройки field (название поля) всех атрибутов приводится к snake_case. Это также справедливо по отношению к внешним ключам и другим автоматически генерируемым полям.
У нас имеется две модели, User и Task, обе с настройками underscored. Между этими моделями установлена ассоциация один-ко-многим. Также, поскольку настройка timestamps по умолчанию имеет значение true, в обеих таблицах будут автоматически созданы поля createdAt и updatedAt.
Без настройки underscored произойдет автоматическое создание:
атрибута createdAt для каждой модели, указывающего на колонку createdAt каждой таблицы
атрибута updatedAt для каждой модели, указывающего на колонку updatedAt каждой таблицы
атрибута userId в модели Task, указыващего на колонку userId таблицы task
С настройкой underscored будут автоматически созданы:
атрибут createdAt для каждой модели, указывающего на колонку created_at каждой таблицы
атрибут updatedAt для каждой модели, указывающего на колонку updated_at каждой таблицы
атрибут userId в модели Task, указыващего на колонку user_id таблицы task
Обратите внимание: в обоих случаях названия полей именуются в стиле camelCase.
Во втором случае вызов sync() приведет к генерации такого SQL:
При добавлении в запрос include, в возвращаемом объекте создается дополнительное поле согласно следующим правилам:
при включении данных из единичной ассоциации (hasOne() или belongsTo()) - название поля указывается в единственном числе
при включении данных из множественной ассоциации (hasMany() или belongsToMany()) - название поля указывается во множественном числе
123456789
// Foo.hasMany(Bar)constfoo=Foo.findOne({include:Bar});// foo.bars будет массивом// Foo.hasOne(Bar)constfoo=Foo.findOne({include:Bar});// foo.bar будет объектом// и т.д.
Кастомизация названий при определении синонимов
При определении синонима для ассоциации вместо { as: 'myAlias' } можно передать объект с единичной и множественной формами таблицы:
При этом, в миксинах будут использоваться правильные формы, например, getLíder(), setLíderes() и т.д.
Обратите внимание: при использовании as для изменения названия ассоциации, также будет изменено название внешнего ключа. Поэтому в данном случае также рекомендуется явно определять название внешнего ключа, причем, в обоих вызовах:
Области видимости (scopes) (далее - области) облегчают повторное использование кода. Они позволяют определить часто используемые настройки, такие как where, include, limit и т.д.
Определение
Области определяются при создании модели и могут быть поисковыми объектами или функциями, возвращающими такие объекты, за исключением дефолтной области, которая может быть только объектом:
Области также могут определяться с помощью метода Model.addScope(). Это может быть полезным при определении областей для включений, когда связанная модель может быть не определена в момент создания основной модели.
Дефолтная область применяется всегда. Это означает, что в приведенном примере вызов Project.findAll() сгенерирует такой запрос:
1
SELECT*FROMprojectsWHEREactive=true;
Дефолтная область может быть удалена с помощью unscoped(), scope(null) или посредством вызова другой области:
1
awaitProject.scope('deleted').findAll();
1
SELECT*FROMprojectsWHEREdeleted=true;
Также имеется возможность включать модели из области в определение области. Это позволяет избежать дублирования include, attributes или where:
Области применяются путем вызова метода scope(scopeName). Этот метод возвращает полнофункциональный экземпляр модели со всеми обычными методами: findAll(), update(), count(), destroy() и т.д.
123456789
constDeletedProjects=Project.scope('deleted');awaitDeletedProjects.findAll();// Это эквивалентно следующемуawaitProject.findAll({where:{deleted:true,},});
Области применяются к find(), findAll(), count(), update(), increment() и destroy().
Области-функции могут вызываться двумя способами. Если область не принимает аргументов, она вызывается как обычно. Если область принимает аргументы, ей передается объект:
При вызове нескольких областей, ключи последующих областей перезаписывают ключи предыдущих областей (по аналогии с Object.assign()), за исключением where и include, в которых ключи объединяются. Рассмотрим две области:
Вызов scope('scope1', 'scope2') приведет к генерации такого предложения WHERE:
1
WHEREfirstName='John'ANDage>30LIMIT10;
Атрибуты limit и age были перезаписаны, а firstName сохранен.
При объединении ключей атрибутов из нескольких областей предполагается attributes.exclude(). Это обеспечивает учет регистра полей при объединении, т.е. сохранение чувствительных полей в финальном результате.
Такая же логика объединения используется при прямой передаче поискового объекта в findAll() (и аналогичные методы):
Эти 4 области легко (и глубоко) объединяются. Например, вызов Foo.scope('includeEverything', 'limitedBars', 'limitedBazs', 'excludedBazName') эквивалентен следующему:
constcreatePostWithReactions=async(content,reactionTypes)=>{constpost=awaitPost.create({content});awaitReaction.bulkCreate(reactionTypes.map((type)=>({type,postId:post.id,})));returnpost;};awaitcreatePostWithReactions('My First Post',['Like','Angry','Laugh','Like','Like','Angry','Sad','Like',]);awaitcreatePostWithReactions('My Second Post',['Laugh','Laugh','Like','Laugh',]);
Допустим, что мы хотим вычислить laughReactionsCount для каждого поста. С помощью подзапроса SQL это можно сделать так:
[{id:1,content:'My First Post',laughReactionsCount:1,},{id:2,content:'My Second Post',laughReactionsCount:3,},];
Sequelize предоставляет специальную утилиту literal() для работы с подзапросами. Данная утилита принимает подзапрос SQL. Т.е. Sequelize помогает с основным запросом, но подзапрос должен быть реализован вручную:
1 2 3 4 5 6 7 8 91011121314151617
Post.findAll({attributes:{include:[[sequelize.literal(`( SELECT COUNT(*) FROM reactions AS reaction WHERE reaction.postId = post.id AND reaction.type = 'Laugh' )`),'laughReactionsCount',],],},});
Результат выполнения данного запроса будет таким же, как при выполнении SQL-запроса.
При использовании подзапросов можно выполнять группировку возвращаемых значений:
1 2 3 4 5 6 7 8 91011121314151617181920
Post.findAll({attributes:{include:[[sequelize.literal(`( SELECT COUNT(*) FROM reactions AS reaction WHERE reaction.postId = post.id AND reaction.type = 'Laugh' )`),'laughReactionsCount',],],},order:[[sequelize.literal('laughReactionsCount'),'DESC'],],});
Результат:
1 2 3 4 5 6 7 8 9101112
[{id:2,content:'My Second Post',laughReactionsCount:3,},{id:1,content:'My First Post',laughReactionsCount:1,},];
Добавление ограничений между таблицами означает, что таблицы должны создаваться в правильном порядке при использовании sequelize.sync(). Если Task содержит ссылку на User, тогда таблица User должна быть создана первой. Иногда это может привести к циклическим ссылкам, когда Sequelize не может определить порядок синхронизации. Предположим, что у нас имеются документы и версии. Документ может иметь несколько версий. Он также может содержать ссылку на текущую версию.
Иногда может потребоваться указать ссылку на другую таблицу без создания ограничений или ассоциаций. В этом случае ссылку и отношения между таблицами можно определить явно:
constTrainer=sequelize.define('Trainer',{firstName:DataTypes.STRING,lastName:DataTypes.STRING,});// `Series` будет содержать внешнюю ссылку `trainerId = Trainer.id`// после вызова `Trainer.hasMany(series)`constSeries=sequelize.define('Series',{title:DataTypes.STRING,subTitle:DataTypes.STRING,description:DataTypes.TEXT,// Определяем отношения один-ко-многим с `Trainer`trainerId:{type:DataTypes.INTEGER,references:{model:Trainer,key:'id',},},});// `Video` будет содержэать внешнюю ссылку `seriesId = Series.id`// после вызова `Series.hasOne(Video)`constVideo=sequelize.define('Video',{title:Sequelize.STRING,sequence:Sequelize.INTEGER,description:Sequelize.TEXT,// Устанавливаем отношения один-ко-многим с `Series`seriesId:{type:DataTypes.INTEGER,references:{model:Series,// Может быть как строкой, представляющей название таблицы, так и модельюkey:'id',},},});Series.hasOne(Video);Trainer.hasMany(Series);
constUser=sequelize.define('User',{/* атрибуты */},{indexes:[// Создаем уникальный индекс для адреса электронной почты{unique:true,fields:['email'],},// Создание обратного индекса для данных с помощью оператора `jsonb_path_ops`{fields:['data'],using:'gin',operator:'jsonb_path_ops',},// По умолчанию название индекса будет иметь вид `[table]_[fields]`// Создаем частичный индекс для нескольких колонок{name:'public_by_author',fields:['author','status'],where:{status:'public',},},// Индекс `BTREE` с сортировкой{name:'title_index',using:'BTREE',fields:['author',{attribute:'title',collate:'en_US',order:'DESC',length:5,},],},],});
Подключение к БД с помощью одного процесса означает создание одного экземпляра Sequelize. При инициализации Sequelize создает пул соединений (connection pool). Этот пул может быть настроен с помощью настройки pool:
При подключении к БД с помощью нескольких процессов, для каждого процесса создается отдельный экземпляр с максимальным размером пула подключений с учетом общего максимального размера.
Подобно тому, как вы используете систему контроля версий, такую как Git, для контроля за изменениями кодовой базы, миграции (migrations) позволяют контролировать изменения, вносимые в БД. Миграции позволяют переводить БД из одного состояния в другое и обратно: изменения состояния сохраняются в файлах миграции, описывающих, как получить новое состояние или как отменить изменения для того, чтобы вернуться к предыдущему состоянию.
Миграция - это JS-файл, из которого экспортируется 2 функции, up и down, описывающие выполнение миграции и ее отмену, соответственно. Эти функции определяются вручную, но вызываются с помощью CLI. В функциях указываются необходимые запросы с помощью sequelize.query() или других методов.
Установка CLI:
123
yarnaddsequelize-cli
# или
npmisequelize-cli
Генерация шаблона
Для создания пустого проекта используется команда init:
1
sequelize-cliinit
Будут созданы следующие директории:
config - файл с настройками подключения к БД
models - модели для проекта
migrations - файлы с миграциями
seeders - файлы для заполнения БД начальными (фиктивными) данными
Настройка
Далее нам нужно сообщить CLI, как подключиться к БД. Для этого откроем файл config/config.json. Он выглядит примерно так:
файл XXXXXXXXXXXXXX-create-user.js в директории migrations
Запуск миграций
Для создания таблицы в БД используется команда db:migrate:
1
sequelize-clidb:migrate
Данная команда выполняет следующее:
создает в БД таблицу SequelizeMeta. Это таблица используется для записи миграций, выполняемых для текущей БД
выполняет поиск файлов с миграциями, которые еще не запускались. В нашем случае будет запущен файл XXXXXXXXXXXXXX-create-user.js
создается таблица Users с колонками, определенными в миграции
Отмена миграций
Для отмены миграций используется команда db:migrate:undo:
1
sequelize-clidb:migrate:undo
Для отмены всех миграций используется команда db:migrate:undo:all, а для отката к определенной миграции - db:migrate:undo:all --to XXXXXXXXXXXXXX-create-posts.js.
Создание скрипта для наполнения БД начальными данными
Предположим, что мы хотим создать дефолтного пользователя в таблице Users. Для управления миграциями данных можно использовать сеятелей (seeders). Засеивание файла - это наполнение таблицы начальными или тестовыми данными.
Создадим файл с кодом, при выполнении которого будет выполняться создание дефолтного пользователя в таблице Users.
1
sequelize-cliseed:generate--namedemo-user
После выполнения этой команды в директории seeders появится файл XXXXXXXXXXXXXX-demo-user.js. В нем используется такая же семантика up / down, что и в файлах миграций.
Для запуска сеятеля используется команда db:seed:all:
1
sequelize-clidb:seed:all
Для отмены последнего сеятеля используется команда db:seed:undo, для отмены определенного сеятеля - db:seed:undo --seed seedName, для отмены всех сеятелей - db:seed:undo:all.
Обратите внимание: отменяемыми являются только те сеятели, которые используют какое-либо хранилище (см. ниже; в отличие от миграций, информация о сеятелях не сохраняется в таблице SequelizeMeta).
Шаблон миграции
Шаблон миграции выглядит так:
12345678
module.exports={up:(queryInterface,Sequelize)=>{// Логика перехода к новому состоянию},down:(queryIntarface,Sequelize)=>{// Логика отмены изменений},};
Мы можем создать этот файл с помощью migration:generate. Эта команда создаст файл xxx-migration-skeleton.js в директории для миграций.
Объект queryInterface используется для модификации БД. Объект Sequelize содержит доступные типы данных, такие как STRING или INTEGER. Функции up() и down() должны возвращать промис. Рассмотрим простой пример создания/удаления таблицы User:
В следующем примере миграция производит два изменения в БД (добавляет две колонки в таблицу User) с помощью управляемой транзакции, обеспечивающей успешное выполнение всех операций или отмену изменений при возникновении ошибки:
Следующий пример демонстрирует создание уникального индекса на основе композиции из нескольких полей с условием, которое позволяет отношениям существовать много раз, но только одно будет удовлетворять условию:
sequelize - хранит миграции и сеятелей в таблице в БД
json - хранит миграции и сеятелей в JSON-файле
none - ничего не хранит
Хранилище миграций
По умолчанию CLI создает в БД таблицу SequelizeMeta для хранения записей о миграциях. Для изменения этого поведения существует 3 настройки, которые можно добавить в файл конфигурации .sequelizerc. Тип хранилища указывается в настройке migrationStorage. При выборе типа json, путь к файлу можно указать в настройке migrationStoragePath (по умолчанию данные будут записываться в файл sequelize-meta.json). Для изменения названия таблицы для хранения информации о миграциях в БД используется настройка migrationStorageTableName. Свойства migrationStorageTableSchema позволяет изменить используемую таблицей SequelizeMeta схему.
1 2 3 4 5 6 7 8 9101112131415161718192021
{"development":{"username":"root","password":null,"database":"database_development","host":"127.0.0.1","dialect":"mysql",// Используем другой тип хранилища. По умолчанию: sequelize"migrationStorage":"json",// Используем другое название файла. По умолчанию: sequelize-meta.json"migrationStoragePath":"sequelizeMeta.json",// Используем другое название таблицы. По умолчанию: SequelizeMeta"migrationStorageTableName":"sequelize_meta",// Используем другую схему для таблицы `SequelizeMeta`"migrationStorageTableSchema":"custom_schema"}}
Хранилище сеятелей
По умолчанию Sequelize не хранит информацию о сеятелях. Настройки файла конфигурации, которые позволяют это изменить:
seederStorage - тип хранилища
seederStoragePath - путь к хранилищу (по умолчанию sequelize-data.json)
seederStorageTableName - название таблицы (по умолчанию SequelizeData)
1 2 3 4 5 6 7 8 9101112131415
{"development":{"username":"root","password":null,"database":"database_development","host":"127.0.0.1","dialect":"mysql",// Определяем другой тип хранилища. По умолчанию: none"seederStorage":"json",// Определяем другое название для файла. По умолчанию: sequelize-data.json"seederStoragePath":"sequelizeData.json",// Определяем другое название для таблицы. По умолчанию: SequelizeData"seederStorageTableName":"sequelize_data"}}