В этой статье будет кратко объяснено, что такое объектно-реляционное сопоставление (ORM), что такое библиотека ORM и почему вам следует рассмотреть возможность ее использования в своем следующем проекте JavaScript. Мы также поможем вам оценить лучшие библиотеки ORM для JavaScript и TypeScript, исходя из ваших потребностей как разработчика и сопровождающего проекта.
Мы рассмотрим каждый из следующих инструментов:
- Knex.js: SQL Query Builder
- Sequelize
- Bookshelf
- Waterline
- Objection.js
- Mongoose
- Typegoose
- TypeORM
- MikroORM
- Prisma
Объектно-реляционное сопоставление
Объектно-реляционное сопоставление может показаться сложным, но его цель — облегчить вашу жизнь как программиста. Чтобы получить данные из базы данных, вам нужно написать запрос. Означает ли это, что вам нужно изучать SQL? Ну нет. Объектно-реляционное сопоставление позволяет писать запросы на любом языке по вашему выбору.
Реляционное сопоставление объектов — это метод преобразования результата запроса к базе данных в экземпляры класса сущности. Объект просто объект обертка для таблицы базы данных. Он содержит атрибуты, сопоставленные столбцам таблицы базы данных. Экземпляры сущностей имеют способы выполнения операций CRUD и поддерживают дополнительные функции, содержащие настраиваемую логику, такую как проверка и шифрование данных.
Если вы создаете небольшой проект, установка библиотеки ORM не требуется. Использование операторов SQL для управления вашим приложением должно быть достаточным. ORM весьма полезен для средних и крупных проектов, которые получают данные из сотен таблиц базы данных. В такой ситуации вам нужна структура, которая позволит вам работать и поддерживать уровень данных вашего приложения согласованным и предсказуемым образом.
Классы сущностей — это строительные блоки бизнес-приложений, поскольку они предназначены для инкапсуляции логики для реализации бизнес-правил. Бизнес-правило определяется для обеспечения того, чтобы автоматизированный процесс выполнялся только в рамках бизнес-политики. Примеры бизнес-правил включают:
- скидки для клиентов
- утверждения ссуд
- комиссионные с продаж
- расчеты по доставке и налогам
Библиотеки ORM
Реляционное отображение объектов обычно выполняется с помощью библиотеки. Термин ORM чаще всего относится к реальной библиотеке ORM — объектному реляционному преобразователю, который выполняет за вас работу по объектному реляционному отображению.
Часто бизнес-правила требуют выполнения нескольких операторов SQL, которые необходимо запускать партиями. Если один оператор SQL не работает, он может оставить базу данных в несогласованном состоянии. Большинство библиотек ORM поддерживают функцию, известную как транзакции, которая предотвращает такие инциденты. Если оператор SQL не может выполняться в контексте транзакции, все другие операторы SQL, которые были успешно выполнены в этом пакете, отменяются посредством операции, известной как откат.
Следовательно, использование библиотеки ORM для построения вашего уровня данных помогает гарантировать, что база данных всегда будет оставаться в согласованном состоянии. Библиотеки ORM часто содержат гораздо больше важных функций, таких как:
- построители запросов
- сценарии миграции
- инструмент CLI для генерации шаблонного кода
- функция заполнения для предварительного заполнения таблиц тестовыми данными
В этой статье я расскажу, как работает каждая библиотека ORM:
- первоначальная настройка и настройка
- основные операции CRUD
- предварительные операции запроса
Я также включил важную информацию, такую как даты запуска, количество пользователей и ссылки на документацию, а также каналы поддержки, если таковые имеются. Я также буду обсуждать важные вопросы, касающиеся производительности запросов, обслуживания библиотек и философии архитектуры, на которые вы должны серьезно повлиять при принятии решения.
А также заказал список по дате запуска от самого раннего до самого нового. Я разделил список на два раздела в зависимости от основного поддерживаемого языка: JavaScript и TypeScript.
Прежде чем мы начнем нашу оценку, давайте сначала взглянем на Knex.js, популярный построитель SQL- запросов, который уже интегрирован с рядом библиотек ORM, перечисленных здесь. Knex.js очень гибкий и часто работает лучше, чем некоторые библиотеки ORM, которые имеют собственную встроенную реализацию построителя запросов. Считайте это преимуществом при выборе библиотеки ORM, в основе которой лежит Knex.js.
Knex.js: построитель SQL-запросов
- Дата запуска: декабрь 2012 г.
- GitHub: используется 158,6 тыс.
- Базы данных: Postgres, MSSQL, MySQL, MariaDB, SQLite3, Oracle и Amazon Redshift
Knex.js в настоящее время является наиболее зрелым конструктором SQL-запросов JavaScript, который может работать как в Node.js, так и в браузере (через webpack или Browserify). Он способен генерировать высокопроизводительные SQL-запросы, которые не уступают написанным вручную операторам SQL.
Так что же такое конструктор запросов?
Это просто API, который предоставляет набор функций, которые можно объединить в цепочку для формирования запроса. Вот пример:
knex({ a: 'table', b: 'table' }) .select({ aTitle: 'a.title', bTitle: 'b.title' }) .whereRaw('?? = ??', ['a.column_1', 'b.column_2']) SQL Output: select `a`.`title` as `aTitle`, `b`.`title` as `bTitle` from `table` as `a`, `table` as `b` where `a`.`column_1` = `b`.`column_2`
Возникает вопрос, почему следует использовать построитель запросов вместо написания необработанных операторов SQL. Я назову вам четыре причины:
- Это помогает вам абстрагироваться от вашего кода из диалекта SQL вашей базы данных, облегчая переключение.
- Это устраняет или значительно снижает вероятность атак SQL-инъекций на ваше приложение.
- Это позволяет легко создавать запросы с динамическими условиями.
- Он поставляется с дополнительными функциями и инструментами CLI для выполнения операций по разработке базы данных.
Эти функции включают:
- пул соединений
- обратный вызов и интерфейсы обещаний
- потоковый интерфейс
- сопровождение сделки
- поддержка схемы
- миграция
- посев
Для его установки в приложение необходимо установить пакет Knex.js вместе с драйвером используемой базы данных:
$ npm install knex --save # Then add one of the following (adding a --save) flag: $ npm install pg $ npm install sqlite3 $ npm install mysql $ npm install mysql2 $ npm install oracledb $ npm install mssql
Вот пример кода настройки:
const knex = require('knex')({ client: 'mysql', connection: { host : '127.0.0.1', user : 'your_database_user', password : 'your_database_password', database : 'myapp_test' } }); knex.schema.createTable('users', function (table) { table.increments(); table.string('name'); table.timestamps(); }) Outputs: create table `users` (`id` int unsigned not null auto_increment primary key, `name` varchar(255), `created_at` datetime, `updated_at` datetime)
Вот пример базового запроса:
knex('users').where({ first_name: 'Test', last_name: 'User' }).select('id') Outputs: select `id` from `users` where `first_name` = 'Test' and `last_name` = 'User'
Также поддерживаются необработанные операторы SQL. Вот пример сложного запроса:
const subcolumn = knex.raw('select avg(salary) from employee where dept_no = e.dept_no') .wrap('(', ') avg_sal_dept'); knex.select('e.lastname', 'e.salary', subcolumn) .from('employee as e') .whereRaw('dept_no = e.dept_no') Outputs: select `e`.`lastname`, `e`.`salary`, (select avg(salary) from employee where dept_no = e.dept_no) avg_sal_dept from `employee` as `e` where dept_no = e.dept_no
Knex.js также поддерживает TypeScript, что здорово, поскольку позволяет писать такой код:
import { Knex, knex } from 'knex' interface User { id: number; age: number; name: string; active: boolean; departmentId: number; } const config: Knex.Config = { client: 'sqlite3', connection: { filename: './data.db', }, }); const knexInstance = knex(config); try { const users = await knex<User>('users').select('id', 'age'); } catch (err) { // error handling }
В приведенном выше примере TypeScript Knex.js работает почти как ORM. Однако экземпляры объекта сущности не создаются. Вместо этого определение интерфейса используется для создания объектов JavaScript с типобезопасными свойствами.
Обратите внимание, что ряд библиотек ORM, перечисленных в этой статье, используют Knex.js под капотом. Они включают:
- Bookshelf
- Objection.js
- MikroORM
Библиотеки ORM часто предоставляют дополнительные функции поверх Knex.js. Давайте посмотрим на них в следующем разделе.
Библиотеки ORM JavaScript
В этой категории все перечисленные здесь библиотеки написаны на JavaScript и могут работать непосредственно в Node.js. Поддержка TypeScript предоставляется либо через встроенные типы, либо через пакет определений @ types / node. Если вам нужна первоклассная поддержка проектов TypeScript, вам следует перейти к разделу » Библиотеки ORM TypeScript».
На уровне доступа к данным используются два популярных архитектурных шаблона:
- Data Mapper
- Active Record
В шаблоне Data Mapper классы сущностей являются чистыми и содержат только атрибуты. Операции CRUD и бизнес-правила реализованы в контейнерах, известных как репозитории. Вот пример:
const repository = connection.getRepository(User);. const user = new User(); user.firstName = "Timber"; await repository.save(user); const allUsers = await repository.find();
При использовании шаблона активной записи логика для операций CRUD и бизнес-правил реализуется в классах сущностей. Вот аналогичный пример реализации вышеизложенного:
const user = new User(); user.firstName = "Timber"; await user.save(); const allUsers = await User.find();
У использования любого шаблона есть свои плюсы и минусы. Эти шаблоны были названы Мартином Фаулером в его книге » Шаблоны архитектуры корпоративных приложений» в 2003 году. Вам следует проверить книгу, если вы хотите получить более подробную информацию по этому вопросу. Большинство библиотек ORM, перечисленных в этой статье, поддерживают один или оба шаблона.
Sequelize
- Launch: July 2010
- GitHub: used by 271k
- Slack
- Databases: Postgres, MySQL, MariaDB, SQLite and Microsoft SQL Server
Sequelize — очень зрелая и популярная ORM-библиотека Node.js с отличной документацией, содержащей хорошо объясненные примеры кода. Он поддерживает многие функции уровня данных, которые мы уже упоминали ранее в предыдущих библиотеках. В отличие от Книжной полки, у него есть собственный Конструктор запросов, который работает так же хорошо, как Knex.js.
Установить библиотеку довольно просто, а драйвер базы данных довольно прост:
$ npm i sequelize # This will install v6 # And one of the following: $ npm i pg pg-hstore # Postgres $ npm i mysql2 $ npm i mariadb $ npm i sqlite3 $ npm i tedious # Microsoft SQL Server
Ниже приведен пример кода установки вместе с примерами CRUD и базовых операторов запроса:
const { Sequelize } = require('sequelize'); // Connect to database const sequelize = new Sequelize('database', 'username', 'password', { host: 'localhost', dialect: /* one of 'mysql' | 'mariadb' | 'postgres' | 'mssql' */ }); // Create Model const User = sequelize.define('User', { // Model attributes are defined here firstName: { type: DataTypes.STRING, allowNull: false }, lastName: { type: DataTypes.STRING // allowNull defaults to true } }, { // Other model options go here }); // Create instance const jane = User.build({ firstName: "Jane", lastName: "Doe" }); await jane.save(); // save to database // Shortcut for creating instance and saving to database at once const jane = await User.create({ firstName: "Jane", lastName: "Doe" }); // Find all users const users = await User.findAll(); console.log(users.every(user => user instanceof User)); // true console.log("All users:", JSON.stringify(users, null, 2));
Ниже приводится пример написания сложного запроса:
// What if you wanted to obtain something like WHERE char_length("content") = 7? Post.findAll({ where: sequelize.where(sequelize.fn('char_length', sequelize.col('content')), 7) }); // SELECT ... FROM "posts" AS "post" WHERE char_length("content") = 7 // A more complex example 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]: 10 }) ] } ] } });
В последнем примере сложного запроса результат SQL был:
SELECT ... FROM "posts" AS "post" WHERE ( char_length("content") = 7 OR "post"."content" LIKE 'Hello%' OR ( "post"."status" = 'draft' AND char_length("content") > 10 ) )
Sequelize поддерживает необработанные операторы SQL, что дает разработчикам гибкость при написании сложных и высокопроизводительных операторов SQL. Результаты также могут быть сопоставлены с экземплярами сущности объекта. Вот пример:
// Callee is the model definition. This allows you to easily map a query to a predefined model const projects = await sequelize.query('SELECT * FROM projects', { model: Projects, mapToModel: true // pass true here if you have any mapped fields }); // Each element of `projects` is now an instance of Project
Основным недостатком Sequelize является то, что разработка замедлилась, и проблемы накапливались, но не решены. К счастью, один из сопровождающих объявил, что с 2021 года библиотека получит должное внимание. Обратите внимание, что все проекты библиотеки ORM в этой статье имеют открытый исходный код и им действительно нужна помощь разработчиков, чтобы сделать их лучше.
Bookshelf
- Launch: March, 2013
- GitHub: Used by 22.4k
- Plugins
- Databases : PostgreSQL, MySQL, and SQLite3
Книжная полка — одна из старейших и самых простых библиотек ORM JavaScript, которые у нас есть. Он построен на основе Knex.js SQL Query Builder и использует множество идей из шаблона Data Mapper. Он предоставляет дополнительные функции, такие как:
- загрузка отношений нетерпеливого и вложенного-нетерпеливого
- полиморфные ассоциации
- поддержка отношений «один к одному», «один ко многим» и «многие ко многим».
К сожалению, нет встроенной поддержки проверки. Однако это может быть реализовано в коде через стороннюю библиотеку, такую как checkit.
Установка Bookshelf в ваш проект выглядит следующим образом:
$ npm install knex $ npm install bookshelf # Then add one of the following: $ npm install pg $ npm install mysql $ npm install sqlite3
Код установки выглядит так:
// Setting up the database connection const knex = require('knex')({ client: 'mysql', connection: { host : '127.0.0.1', user : 'your_database_user', password : 'your_database_password', database : 'myapp_test', charset : 'utf8' } }) const bookshelf = require('bookshelf')(knex) // Define User model const User = bookshelf.model('User', { tableName: 'users', posts() { return this.hasMany(Posts) } }) // Define Post model const Post = bookshelf.model('Post', { tableName: 'posts', tags() { return this.belongsToMany(Tag) } }) // Define Tag model const Tag = bookshelf.model('Tag', { tableName: 'tags' }) // Unfortunate example of unreadable code new User({id: 1}).fetch({withRelated: ['posts.tags']}).then((user) => { console.log(user.related('posts').toJSON()) }).catch((error) => { console.error(error) })
Вам нужно будет посмотреть документацию Knex.js, чтобы узнать, как выполнять запросы и транзакции CRUD. Документация книжной полки не охватывает этого.
Интересно, что Strapi, автономная CMS, использует Книжную полку в качестве коннектора базы данных по умолчанию. Однако стоит отметить следующие моменты:
документация не особо полезна
на момент написания библиотека не обновлялась пять месяцев
Waterline
- Launch: May 2013
- GitHub: Used by 8.5k
- Documentation
- Databases : Local disk/memory, MySQL, MongoDB, and Postgres(official adapters)
- Community Database Adapters: Oracle, SAP, Cassandra, IBM, Apache Derby, Redis, Solr and more
Waterline — это ORM по умолчанию, используемый Sails.js, фреймворком Node.js. При использовании Sails.js для разработки вашего проекта объем кода, который вам нужно написать для создания собственного API базы данных, значительно сокращается. Это достигается с помощью философии «соглашение над конфигурацией» и API Blueprints, который содержит шаблонный код для доступа к базе данных и выполнения функций CRUD. Кроме того, Sails.js предоставляет интерфейс командной строки, который помогает разработчикам генерировать маршруты API, выполнять миграции и другие функции уровня данных. Поддержка машинописного текста доступна через пакет типизированных определений.
В этой статье мы предполагаем, что вы хотите использовать ORM Waterline как отдельную программу, что вполне возможно. Давайте посмотрим, как его установить и настроить.
Для установки необходимо установить библиотеку Waterline, а затем один из адаптеров базы данных:
$ npm install --save waterline # Install database adapters $ npm install --save sails-mysql $ npm install --save-dev sails-disk
Вот частичный образец кода установки:
const Waterline = require('waterline'); const sailsDiskAdapter = require('sails-disk'); const waterline = new Waterline(); const userCollection = Waterline.Collection.extend({ identity: 'user', datastore: 'default', primaryKey: 'id', attributes: { id: { type: 'number', autoMigrations: {autoIncrement: true} }, firstName: {type:'string'}, lastName: {type:'string'}, // Add a reference to Pets pets: { collection: 'pet', via: 'owner' } } }); waterline.registerModel(userCollection);
Вот частичный образец некоторого кода CRUD:
(async ()=>{ // First we create a user var user = await User.create({ firstName: 'Neil', lastName: 'Armstrong' }); // Then we create the pet var pet = await Pet.create({ breed: 'beagle', type: 'dog', name: 'Astro', owner: user.id }); // Then we grab all users and their pets var users = await User.find().populate('pets'); })()
Вот пример базового кода запроса:
var thirdPageOfRecentPeopleNamedMary = await Model.find({ where: { name: 'mary' }, skip: 20, limit: 10, sort: 'createdAt DESC' });
Когда дело доходит до обработки сложных запросов, кажется, что в документации отсутствует эта часть. Если вы планируете использовать Sails.js, использование Waterline ORM не составит труда. Но как отдельная библиотека ORM сталкивается со следующими проблемами:
Objection.js
- Launch: April 2015
- GitHub: Used by 5.7k
- Plugins
- Databases : SQLite3, Postgres and MySQL (including all Knex.js supported databases)
Objection.js — это минимальная ORM-библиотека Node.js, разработанная, чтобы не мешать вам и упростить доступ к базам данных SQL. В этой категории Objection.js является самым молодым и, похоже, опровергает многие аргументы, выдвинутые против использования библиотек ORM.
Документация Objection.js отличная. Он хорошо написан, так как вы можете легко найти четкие инструкции по созданию уровня данных вашего приложения. Синтаксис чистый и легкий для понимания. Он построен на основе Knex.js и имеет официальную встроенную поддержку TypeScript. В нем есть все, что вам нужно в ORM.
Глядя на цифры, довольно удивительно, что Objection.js не так популярен, как должен быть. Библиотеки ORM, такие как Sequelize и TypeORM, предлагают гораздо больше функций, что может объяснить их популярность. Однако я думаю, что набор функций, которые решила использовать команда Objection.js, идеально подходит для библиотеки с открытым исходным кодом. Это означает, что со временем возникает меньше ошибок, и небольшая команда может своевременно их устранять. Вы можете убедиться в этом, посмотрев на вкладку » Проблемы «, на момент написания которой было около 50 нерешенных проблем.
Напротив, Sequelize и TypeORM, более крупные с точки зрения возможностей, к сожалению, вызвали огромное отставание для своих сопровождающих. В настоящее время у каждого из них более 1000 проблем, которые не были решены, и, похоже, не наблюдается увеличения числа сопровождающих, участвующих в проекте.
Если у вас возникли какие — либо сомнения по поводу выбора этой библиотеки, проверить это можете прочитать отзывы ссылку.
Давайте посмотрим на шаги установки и некоторый пример кода. Для начала вам необходимо установить Objection.js, Knex.js и один из адаптеров базы данных:
npm install objection knex # Install database adapter npm install pg npm install sqlite3 npm install mysql npm install mysql2
Код настройки настолько прост, что не требует объяснений:
const { Model } = require('objection'); const Knex = require('knex'); // Initialize knex. const knex = Knex({ client: 'sqlite3', useNullAsDefault: true, connection: { filename: 'example.db' } }); // Give the Knex instance to Objection. Model.knex(knex); // Person model. class Person extends Model { static get tableName() { return 'persons'; } static get relationMappings() { return { children: { relation: Model.HasManyRelation, modelClass: Person, join: { from: 'persons.id', to: 'persons.parentId' } } }; } } async function createSchema() { if (await knex.schema.hasTable('persons')) { return; } // Create database schema. You should use Knex migration files // to do this. We create it here for simplicity. await knex.schema.createTable('persons', table => { table.increments('id').primary(); table.integer('parentId').references('persons.id'); table.string('firstName'); }); } async function main() { // Create some people. const sylvester = await Person.query().insertGraph({ firstName: 'Sylvester', children: [ { firstName: 'Sage' }, { firstName: 'Sophia' } ] }); console.log('created:', sylvester); // Fetch all people named Sylvester and sort them by ID. // Load `children` relation eagerly. const sylvesters = await Person.query() .where('firstName', 'Sylvester') .withGraphFetched('children') .orderBy('id'); console.log('sylvesters:', sylvesters); } createSchema() .then(() => main()) .then(() => knex.destroy()) .catch(err => { console.error(err); return knex.destroy(); });
Вот пример базового запроса:
// query 1 const person = await Person.query().findById(1); //query 2 const middleAgedJennifers = await Person.query() .select('age', 'firstName', 'lastName') .where('age', '>', 40) .where('age', '<', 60) .where('firstName', 'Jennifer') .orderBy('lastName');
Вывод SQL для базового запроса:
-- query 1 select "persons".* from "persons" where "persons"."id" = 1 -- query 2 select "age", "firstName", "lastName" from "persons" where "age" > 40 and "age" < 60 and "firstName" = 'Jennifer' order by "lastName" asc
Вот пример сложного запроса:
const people = await Person.query() .select('persons.*', 'parent.firstName as parentFirstName') .innerJoin('persons as parent', 'persons.parentId', 'parent.id') .where('persons.age', '<', Person.query().avg('persons.age')) .whereExists( Animal.query() .select(1) .whereColumn('persons.id', 'animals.ownerId') ) .orderBy('persons.lastName'); console.log(people[0].parentFirstName);
Вывод SQL для сложного запроса:
select "persons".*, "parent"."firstName" as "parentFirstName" from "persons" inner join "persons" as "parent" on "persons"."parentId" = "parent"."id" where "persons"."age" < ( select avg("persons"."age") from "persons" ) and exists ( select 1 from "animals" where "persons"."id" = "animals"."ownerId" ) order by "persons"."lastName" asc
В дополнение к функциям, которые уже предоставляет Knex.js, Objection.js имеет:
- официальная поддержка TypeScript
- поддержка хуков жизненного цикла
- встроенная поддержка проверки с использованием синтаксиса схемы JSON
- плагины
Библиотека в очень хорошем состоянии. Для баз данных SQL Objection.js кажется лучшей библиотекой ORM для вашего приложения JavaScript. К сожалению, он не поддерживает базы данных NoSQL. Но следующая библиотека, которую мы представим, поддерживает базы данных NoSQL.
Mongoose
- Launch: April 2010
- GitHub: Used by 1.4m
- Slack
- Plugins
- Databases : MongoDB
Если вы планируете использовать MongoDB в качестве базы данных, то Mongoose, вероятно, станет вашим предпочтительным ORM. В настоящее время это самая популярная библиотека ORM в мире Node.js. Mongoose использует синтаксис схемы для определения моделей. Список его функций включает:
- встроенное литье типов
- Проверка
- построение запросов
- перехватчики через промежуточное ПО
Mongoose поддерживает только MongoDB, поэтому для установки требуется только один пакет:
npm install mongoose
Ниже приведен пример кода установки:
const mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/test', {useNewUrlParser: true, useUnifiedTopology: true}); // With Mongoose, everything is derived from a Schema. const kittySchema = new mongoose.Schema({ name: { type: String, required: true } }); const Kitten = mongoose.model('Kitten', kittySchema); const fluffy = new Kitten({ name: 'fluffy' }); fluffy.save(function (err, fluffy) { if (err) return console.error(err); console.log(fluffy.name, 'saved!') });
В Mongoose есть два способа определения запросов. Ниже приведены оба примера:
// With a JSON doc Person. find({ occupation: /host/, 'name.last': 'Ghost', age: { $gt: 17, $lt: 66 }, likes: { $in: ['vaporizing', 'talking'] } }). limit(10). sort({ occupation: -1 }). select({ name: 1, occupation: 1 }). exec(callback); // Using query builder Person. find({ occupation: /host/ }). where('name.last').equals('Ghost'). where('age').gt(17).lt(66). where('likes').in(['vaporizing', 'talking']). limit(10). sort('-occupation'). select('name occupation'). exec(callback);
Конечно, здесь нет варианта с исходным SQL, поскольку MongoDB — это база данных NoSQL. MongoDB также не поддерживает транзакции. Если это важно для вашего проекта, вам нужно придерживаться баз данных SQL.
Одним из ключевых преимуществ Mongoose перед всеми другими перечисленными здесь библиотеками ORM с открытым исходным кодом является то, что ее разработка спонсируется платформой Tidelift. Это означает, что проблемы безопасности выявляются и исправляются на раннем этапе.
Одним из недостатков является то, что Mongoose официально не поддерживает TypeScript. Неофициально вы можете использовать TypeScript, но для поддержки ваших моделей потребуется немного дополнительной работы. К счастью, следующая библиотека ORM, которую мы рассмотрим, решит эту проблему.
Библиотеки ORM TypeScript
В этой категории все перечисленные здесь библиотеки обеспечивают первоклассную поддержку проектов TypeScript. Вы можете использовать их в проектах JavaScript, но документация в основном написана для кода TypeScript.
Typegoose
- Launch: March 2017
- GitHub: Used by 2k
- Databases: MongoDB
Typegoose — это «оболочка» для простого написания моделей Mongoose с помощью TypeScript. Эта библиотека решает проблему необходимости поддерживать отдельную модель Mongoose и интерфейс TypeScript. С Typegoose вам нужно только определить схему вашей модели с помощью интерфейса Typegoose.
Под капотом он использует API Reflect и Reflect-metadata для извлечения типов свойств, поэтому избыточность может быть значительно уменьшена.
Для установки Typegoose в ваши проекты требуется несколько пакетов:
npm i -s @typegoose/typegoose # install typegoose itself npm i -s mongoose # install peer-dependency mongoose npm i -D @types/mongoose # install all types for mongoose
Ниже приведен пример модели Mongoose, написанной на JavaScript:
const kittenSchema = new mongoose.Schema({ name: String }); const Kitten = mongoose.model('Kitten', kittenSchema); let document = await Kitten.create({ name: 'Kitty' }); // "document" has no types
Ниже представлена та же модель, написанная на TypeScript с использованием библиотеки Typegoose:
class KittenClass { @prop() public name?: string; } const Kitten = getModelForClass(KittenClass); let document = await Kitten.create({ name: 'Kitty' }); // "document" has proper types of KittenClass
В следующем примере кода показан процесс настройки и выполнение команд CRUD:
import { prop, getModelForClass } from '@typegoose/typegoose'; import * as mongoose from 'mongoose'; class User { @prop() public name?: string; @prop({ type: () => [String] }) public jobs?: string[]; } const UserModel = getModelForClass(User); // UserModel is a regular Mongoose Model with correct types (async () => { await mongoose.connect('mongodb://localhost:27017/', { useNewUrlParser: true, useUnifiedTopology: true, dbName: "test" }); const { _id: id } = await UserModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an "as" assertion, to have types for all properties const user = await UserModel.findById(id).exec(); console.log(user); // prints { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 } })();
Поскольку Typegoose — это просто оболочка TypeScript для библиотеки ORM, вам нужно будет посмотреть документацию Mongoose, чтобы узнать, как выполнять задачи CRUD.
TypeORM
- Launch : Feb 21, 2016
- GitHub: Used by 71.8k
- Slack
- Databases : MySQL, MariaDB, Postgres, CockroachDB, SQLite, Microsoft SQL Server, Oracle, SAP Hana, sql.js and MongoDB
TypeORM в настоящее время является самой популярной библиотекой ORM, созданной для проектов TypeScript. Он может работать на многих платформах, в том числе:
- Node.js
- браузер
- на мобильных устройствах — Cordova, PhoneGap, Ionic, React Native и NativeScript
- Electron
Библиотека также поддерживает шаблоны Active Record и Data Mapper, что позволяет разработчикам создавать высококачественные, масштабируемые и удобные в обслуживании приложения, управляемые базами данных. На него сильно влияют другие ORM, такие как Hibernate, Doctrine и Entity Framework. Это означает, что разработчики с опытом работы на Java и Ruby будут чувствовать себя как дома.
TypeORM — это ORM, который может работать на платформах Node.js, браузере, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo и Electron, а также может использоваться с TypeScript и JavaScript. Его цель — всегда поддерживать новейшие функции JavaScript и предоставлять дополнительные функции, которые помогут вам разрабатывать любые приложения, использующие базы данных — от небольших приложений с несколькими таблицами до крупномасштабных корпоративных приложений с несколькими базами данных.
Для установки TypeORM требуется установить несколько пакетов, включая адаптеры баз данных и дополнительные пакеты TypeScript:
npm install typeorm --save # You need to install reflect-metadata shim: npm install reflect-metadata --save # and import it somewhere in the global place of your app (for example in app.ts): # import "reflect-metadata"; # You may need to install node typings: npm install @types/node --save-dev # Install a database driver: npm install mysql --save (you can install mysql2 instead as well) npm install pg --save npm install sqlite3 --save npm install mssql --save npm install sql.js --save # To make the Oracle driver work, you need to follow the installation instructions from their site. npm install oracledb --save # for SAP Hana npm i @sap/hana-client npm i hdb-pool # for MongoDB (experimental) npm install mongodb --save
Далее вам нужно будет включить следующие настройки в tsconfig.json:
"emitDecoratorMetadata": true, "experimentalDecorators": true,
Вам также может потребоваться включить es6в libразделе параметров компилятора или установить es6-shimиз @types.
В качестве альтернативы, вместо того, чтобы вручную настраивать проект TypeORM, вы можете просто использовать инструмент TypeORM CLI, чтобы сформировать проект за вас:
npm install typeorm -g typeorm init --name MyProject --database mysql
Модели можно определять с помощью реализации DataMapper:
// Define entity model first import {Entity, PrimaryGeneratedColumn, Column} from "typeorm"; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() firstName: string; @Column() lastName: string; @Column() age: number; } // Perform CRUD tasks const repository = connection.getRepository(User); const user = new User(); user.firstName = "Timber"; user.lastName = "Saw"; user.age = 25; await repository.save(user); const allUsers = await repository.find(); const firstUser = await repository.findOne(1); // find by id const timber = await repository.findOne({ firstName: "Timber", lastName: "Saw" }); await repository.remove(timber);
В качестве альтернативы вы можете использовать шаблон Active Record для определения ваших моделей:
import {Entity, PrimaryGeneratedColumn, Column, BaseEntity} from "typeorm"; @Entity() export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; @Column() firstName: string; @Column() lastName: string; @Column() age: number; } const user = new User(); user.firstName = "Timber"; user.lastName = "Saw"; user.age = 25; await user.save(); const allUsers = await User.find(); const firstUser = await User.findOne(1); const timber = await User.findOne({ firstName: "Timber", lastName: "Saw" }); await timber.remove();
TypeORM предоставляет несколько способов создания запросов с помощью собственного Query Builder. Вот один из его примеров:
const firstUser = await connection .getRepository(User) .createQueryBuilder("user") .where("user.id = :id", { id: 1 }) .getOne();
Ниже представлен вывод SQL:
SELECT user.id as userId, user.firstName as userFirstName, user.lastName as userLastName FROM users user WHERE user.id = 1
Вот пример сложного запроса:
const posts = await connection.getRepository(Post) .createQueryBuilder("post") .where(qb => { const subQuery = qb.subQuery() .select("user.name") .from(User, "user") .where("user.registered = :registered") .getQuery(); return "post.title IN " + subQuery; }) .setParameter("registered", true) .getMany();
Хотя TypeORM, кажется, охватывает все функции, необходимые для создания уровня данных для вашего приложения, есть несколько острых проблем, о которых вам следует знать. Самый примечательный из них касается производительности, о которой было сообщено и задокументировано в этой нерешенной проблеме.
Из-за огромного количества функций, которые поддерживает библиотека, накопившийся объем нерешенных проблем достиг значительного уровня, что ложится тяжелым бременем на разработчиков ядра. Этот вопрос был рассмотрен разработчиками здесь, где они обсуждают будущее TypeORM.
Тем не менее, TypeORM в настоящее время является самой популярной ORM для TypeScript. Это означает, что найти разработчиков, знакомых с библиотекой, будет проще, когда дело доходит до поддержки вашего проекта в долгосрочной перспективе. Надеюсь, что больше участников присоединятся к основной команде обслуживания и помогут стабилизировать ORM.
MikroORM
- Launch: Mar 11, 2018
- GitHub: Used by 206
- Slack
- Databases : MongoDB, MySQL, MariaDB, PostgreSQL and SQLite
MikroORM — один из самых молодых участников Node.js TypeScript ORM в этом списке. Он поддерживает как базы данных SQL, так и NoSQL, что является удивительным достижением, которого удалось достичь не многим ORM. Он во многом вдохновлен Doctrine и Nextras ORM. Любой, кто знаком с ними, должен чувствовать себя с MikroORM как дома.
Библиотека оптимизирована для транзакций и производительности с помощью шаблонов Identity Map. Он также поддерживает шаблон Data Mapper. Документация отлично, с легкой навигацией по конкретным темам. Одним из ключевых преимуществ использования новых библиотек ORM является то, что они предназначены для преодоления многих архитектурных проблем, с которыми сталкиваются старые и большие библиотеки.
Просматривая образцы кода, которые я предоставил, вы заметите, что синтаксис намного проще для понимания. Это ключ к созданию крупномасштабных проектов, которые будут поддерживать в долгосрочной перспективе. Теперь пройдемся по процессу установки:
npm i -s @mikro-orm/core @mikro-orm/mongodb # for mongo npm i -s @mikro-orm/core @mikro-orm/mysql # for mysql npm i -s @mikro-orm/core @mikro-orm/mariadb # for mariadb npm i -s @mikro-orm/core @mikro-orm/postgresql # for postgresql npm i -s @mikro-orm/core @mikro-orm/sqlite # for sqlite
Далее вам нужно включить поддержку декораторов и esModuleInteropв tsconfig.json:
"experimentalDecorators": true, "emitDecoratorMetadata": true, "esModuleInterop": true,
Затем вызовите MikroORM.initкак часть начальной загрузки вашего приложения:
const orm = await MikroORM.init({ entities: [Author, Book, BookTag], dbName: 'my-db-name', type: 'mongo', // one of `mongo` | `mysql` | `mariadb` | `postgresql` | `sqlite` clientUrl: '...', // defaults to 'mongodb://localhost:27017' for mongodb driver }); console.log(orm.em); // access EntityManager via `em` property
MikroORM предоставляет инструмент командной строки @mikro-orm/cli, доступ к которому вы можете получить, используя npxили установив локально и получив доступ к нему следующим образом:
# manually $ node node_modules/.bin/mikro-orm # via npx $ npx mikro-orm # or via yarn $ yarn mikro-orm
Инструмент командной строки помогает в процессе разработки и может помочь вам в выполнении таких задач, как:
- управление схемой
- импорт файла SQL в базу данных
- генерирующие объекты
- миграция базы данных
MikroORM предоставляет три способа определения классов сущностей. Вот один пример использования синтаксиса метаданных отражения:
@Entity() export class Book extends BaseEntity { @Property() title!: string; @ManyToOne(() => Author) author!: Author; @ManyToOne(() => Publisher, { wrappedReference: true, nullable: true }) publisher?: IdentifiedReference<Publisher>; @ManyToMany({ entity: 'BookTag', fixedOrder: true }) tags = new Collection<BookTag>(this); }
После того, как вы определили свои сущности, вы можете использовать диспетчер сущностей для сохранения и запроса ваших данных:
// use constructors in your entities for required parameters const author = new Author('Jon Snow', 'snow@wall.st'); author.born = new Date(); const publisher = new Publisher('7K publisher'); const book1 = new Book('My Life on The Wall, part 1', author); book1.publisher = publisher; const book2 = new Book('My Life on The Wall, part 2', author); book2.publisher = publisher; const book3 = new Book('My Life on The Wall, part 3', author); book3.publisher = publisher; // just persist books, author and publisher will be automatically cascade persisted await orm.em.persistAndFlush([book1, book2, book3]); // or one by one orm.em.persist(book1); orm.em.persist(book2); orm.em.persist(book3); await orm.em.flush(); // flush everything to database at once // Update existing book const book = await orm.em.findOne(Book, 1); book.title = 'How to persist things...'; // no need to persist `book` as its already managed by the EM await orm.em.flush(); // Retrieve all books const books = await orm.em.find(Book, {}); for (const book of books) { console.log(book.title); }
Запросы сущностей могут выполняться с помощью объекта условий, известного как FilterQuery. Вот разные примеры:
// search by entity properties const users = await orm.em.find(User, { firstName: 'John' }); // for searching by reference you can use primary key directly const id = 1; const users = await orm.em.find(User, { organization: id }); // or pass unpopulated reference (including `Reference` wrapper) const ref = await orm.em.getReference(Organization, id); const users = await orm.em.find(User, { organization: ref }); // fully populated entities as also supported const ent = await orm.em.findOne(Organization, id); const users = await orm.em.find(User, { organization: ent }); // complex queries with operators const users = await orm.em.find(User, { $and: [{ id: { $nin: [3, 4] } }, { id: { $gt: 2 } }] }); // you can also search for array of primary keys directly const users = await orm.em.find(User, [1, 2, 3, 4, 5]); // and in findOne all of this works, plus you can search by single primary key const user1 = await orm.em.findOne(User, 1);
Библиотека также поддерживает:
- получение частичных сущностей
- получение результатов с разбивкой на страницы
- с использованием пользовательских фрагментов SQL
Чтобы выполнять еще более сложные запросы, вы можете использовать Конструктор запросов. Вот пример кода:
const qb = orm.em.createQueryBuilder(Author); qb.update({ name: 'test 123', type: PublisherType.GLOBAL }).where({ id: 123, type: PublisherType.LOCAL }); console.log(qb.getQuery()); // update `publisher2` set `name` = ?, `type` = ? where `id` = ? and `type` = ? console.log(qb.getParams()); // ['test 123', PublisherType.GLOBAL, 123, PublisherType.LOCAL] // run the query const res1 = await qb.execute();
Построитель запросов MikroORM использует Knex.js, к которому вы можете получить доступ через qb.getKnexQuery()функцию. Это означает, что могут быть выполнены все сложные и необработанные SQL-запросы, которые вы хотите создать и выполнить. Следовательно, вы получаете преимущества гибкости и производительности, выбирая MikroORM в своем техническом стеке. В документации по Query Builder есть много примеров построения запросов, включая различные типы объединений, которых слишком много, чтобы перечислять здесь. Вам будет приятно узнать, что построитель запросов предоставляет функцию для отображения выходных данных SQL во время разработки без включения параметра отладки. Вот пример:
const qb = orm.em.createQueryBuilder(BookTag, 't'); qb.select(['b.*', 't.*']) .leftJoin('t.books', 'b') .where('b.title = ? or b.title = ?', ['test 123', 'lol 321']) .andWhere('1 = 1') .orWhere('1 = 2') .limit(2, 1); console.log(qb.getQuery()); // select `b`.*, `t`.*, `e1`.`book_tag_id`, `e1`.`book_uuid_pk` from `book_tag` as `t` // left join `book_to_book_tag` as `e1` ON `t`.`id` = `e1`.`book_tag_id` // left join `book` as `b` ON `e1`.`book_uuid_pk` = `b`.`uuid_pk` // where (((b.title = ? or b.title = ?) and (1 = 1)) or (1 = 2)) // limit ? offset ?
Одна из проблем, вызывающих беспокойство, заключается в том, что библиотека довольно молода и количество пользователей невелико. Однако основатель библиотеки включил функцию спонсора GitHub, которая позволяет им собирать средства, чтобы они могли работать над проектом полный рабочий день. Я считаю, что это лучший подход к разработке с открытым исходным кодом, чем необходимость работать неполный рабочий день над другим проектом. Имея штатных разработчиков, работающих над проектом с открытым исходным кодом, они могут сосредоточиться на поддержании качества библиотеки и минимизации количества невыполненных работ. Я очень надеюсь, что скоро у них появится крупный спонсор.
Prisma
- Launch: April 2019
- GitHub: Used by 5.7k
- Databases : PostgreSQL, MySQL, SQLite, SQL Server
Prisma — это самая последняя версия ORM TypeScript в этой статье. Он описывает себя как «ORM следующего поколения», который упрощает работу с базами данных для разработчиков приложений. Он предоставляет следующие инструменты:
- Prisma Client: клиентская библиотека, обеспечивающая безопасный доступ к базе данных.
- Prisma Migrate ( предварительная версия ): инструмент миграции, который автоматически создается при внесении изменений в файл схемы.
- А также Prisma Studio: современный графический интерфейс для просмотра и управления данными в вашей базе данных
Prisma сильно отличается от всех других ORM, которые мы рассматривали. Он не использует объектные модели (классы сущностей), а скорее файл схемы для сопоставления всех таблиц и столбцов. Этот файл используется средством миграции для создания файла миграции SQL и клиентской библиотекой для создания определений типов. Все сгенерированные определения типов хранятся в.prisma/client/index.d.tsпапке. Вот пример сгенерированного представления для Userтипа:
export declare type User = { id: string email: string name: string | null }
Вы могли заметить, что postsссылка в модели отсутствует в определении TypeScript. Рекомендуемое решение — создать разновидность такого Userтипа:
import { Prisma } from '@prisma/client' // Define a type that includes the relation to `Post` type UserWithPosts = Prisma.UserGetPayload<{ include: { posts: true } }>
Когда вы пишете запрос, ваш код будет проверен, чтобы убедиться, что вы не ссылаетесь на несуществующее свойство и что вы назначаете правильный тип данных для каждого свойства. Когда вы выполняете запрос, все результаты будут возвращены в виде простых объектов JavaScript.
Традиционные ORM обеспечивают объектно-ориентированный способ работы с реляционными базами данных путем сопоставления таблиц с классами моделей на вашем языке программирования. Такой подход приводит ко многим проблемам, вызванным несоответствием объектно-реляционного импеданса.
Настройка проекта Prisma — это небольшой процесс, полные инструкции по которому вы можете найти здесь. Пока мы просто оцениваем. Вот основные этапы установки:
npm install prisma typescript ts-node @types/node --save-dev
Вам нужно будет выполнить обновление tsconfig.jsonследующим образом:
{ "compilerOptions": { "sourceMap": true, "outDir": "dist", "strict": true, "lib": ["esnext"], "esModuleInterop": true } }
Начните с создания модели данных вашего приложения в файле схемы, расположенном по адресу prisma/schema.prisma:
datasource db { provider = "postgresql" url = env("DATABASE_UR } model Post { id Int @default(autoincrement()) @id createdAt DateTime @default(now()) updatedAt DateTime @updatedAt title String @db.VarChar(255) content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId Int } model Profile { id Int @default(autoincrement()) @id bio String? user User @relation(fields: [userId], references: [id]) userId Int @unique } model User { id Int @default(autoincrement()) @id email String @unique name String? posts Post[] profile Profile? }
Затем вам нужно сопоставить вашу модель данных со схемой базы данных с помощью prisma migrateинструмента CLI:
npx prisma migrate dev --name init --preview-feature
Мы пропустим процесс установки и посмотрим на наш установочный код index.ts:
import { PrismaClient } from '@prisma/client' const prisma = new PrismaClient() async function main() { const allUsers = await prisma.user.findMany() console.log(allUsers) } main() .catch(e => { throw e }) .finally(async () => { await prisma.$disconnect() })
Ниже приведен пример, демонстрирующий, как сохранять данные и записи запросов:
async function main() { await prisma.user.create({ data: { name: 'Alice', email: 'alice@prisma.io', posts: { create: { title: 'Hello World' }, }, profile: { create: { bio: 'I like turtles' }, }, }, }) const allUsers = await prisma.user.findMany({ include: { posts: true, profile: true, }, }) console.dir(allUsers, { depth: null }) }
Когда вы запустите приведенный выше код, результаты будут возвращены в виде объектов JavaScript, например:
[ { email: 'alice@prisma.io', id: 1, name: 'Alice', posts: [ { content: null, createdAt: 2020-03-21T16:45:01.246Z, id: 1, published: false, title: 'Hello World', authorId: 1, } ], profile: { bio: 'I like turtles', id: 1, userId: 1, } } ]
Документация Prisma выглядит красиво и, похоже, содержит много контента. К сожалению, мне было сложно найти нужную информацию. Либо это из-за слишком сложной системы навигации, либо из-за отсутствия конкретного контента. Информация распределена по нескольким разделам, включая:
- концепции
- гиды
- Справка
- статьи поддержки / справки
Prisma — это более новая библиотека, которая следует другой философии построения уровня данных. Кроме того, похоже, что он растет быстрее, чем MikroORM, тем более, что он был запущен годом позже.
Заключение
В заключение я хотел бы кратко обсудить доводы против использования библиотек ORM в вашем проекте. К основным аргументам можно отнести:
громоздкие, неэффективные запросы
разочарование при использовании библиотеки
проблемы миграции: поддержание синхронизации классов сущностей и схемы базы данных
потеря безопасности типов при использовании опции raw SQL
Вы можете прочитать все аргументы против использования библиотек ORM здесь и здесь.
Изучив все текущие библиотеки ORM JavaScript и TypeScript, вы должны знать, что каждая из них отличается своей реализацией. Большинство аргументов против библиотек ORM было разрешено новыми, такими как Object.js и Prisma. Если вы решите не использовать библиотеку ORM, вам придется выбрать отдельные инструменты и библиотеки, которые составляют ваш стек уровня данных.
На мой взгляд, выбор ORM для вашего проекта — лучшее решение по одной причине: документации.
Как разработчики, мы очень плохо документируем собственный код. Если бы мы реализовали индивидуальное решение или внедрили библиотеку, которая не очень хорошо известна, будущим сопровождающим было бы сложно поддерживать ваше приложение в соответствии с потребностями бизнеса.
Однако, если вы используете хорошо документированную библиотеку ORM, им станет намного проще работать с вашим приложением еще долго после того, как вы покинули проект. Это связано с тем, что ORM прививают хорошие практики кода, такие как архитектура и шаблоны, такие как Data Mapper. И хотя это может потребовать обучения, в долгосрочной перспективе это лучше.
Надеюсь, я предоставил полезную информацию, которая поможет вам оценить библиотеку ORM для вашего проекта. Если вам нужна рекомендация, выберите библиотеку ORM TypeScript, которая больше всего подходит для проекта корпоративного класса.