Lubejs — это библиотека для
node.js
, предназначенная для удобной работы с соединением SQL-баз данных. Название lube происходит от английского слова «lubricant», что означает «смазка». Таким образом, Lubejs выступает в роли смазки между JavaScript и SQL, позволяющей использовать элегантный JavaScript/TypeScript вместо конкатенации строк SQL.
Настоящая библиотека частично вдохновлена EF и TypeORM, за что мы благодарны.
Lubejs — это набор типизированного построения и выполнения SQL-запросов, а также мощный и удобный фреймворк ORM на TypeScript.- Полностью функциональный инструмент для построения SQL, который позволяет писать запросы на языке, максимально приближенном к SQL, обеспечивая низкий порог входа.
Поддержка мощных типов TypeScript, включая обратное вывод типов, что гарантирует четкие типы выходных данных и полную систему типовой безопасности, а также улучшенные возможности автодополнения, повышающие производительность разработки и снижающие риск ошибок типа. Мы настоятельно рекомендуем использовать Lubejs в проектах на TypeScript.
Инструменты ORM, такие как Code First и управление миграциями данных.
Совместимость с несколькими базами данных (в настоящее время поддерживаются только MSSQL).
Кросс-платформенная совместимость, для которой Lubejs предлагает стандартную библиотеку поведений, которая объединяет большинство часто используемых операций, отличающихся в различных базах данных.## Концепция Lubejs
Минимальизм, простой API, легкий вход.
Природность, синтаксис максимально приближен к стандартному SQL, что значительно снижает затраты времени на обучение.
Пошаговое внедрение, Lubejs состоит из двух уровней использования: core и полный набор функций.
Универсальная совместимость с различными базами данных, создание промежуточных стандартных библиотек операций и постоянное их расширение.
Полная система типовой безопасности на TypeScript
Установите библиотеку с помощью npm:
# Установка библиотеки Lubejs
npm install lubejs --save
# Установка драйвера Lubejs-MSSQL
npm install lubejs-mssql
Пример Hello World!
// hello-world.ts
import { connect, SQL } from 'lubejs';
// импорт драйвера mssql
import 'lubejs-mssql';
(async () => {
// Создание соединения
const db = await connect('mssql://user:password@localhost:1433/database');
// SELECT 'hello world'
console.log(await db.queryScalar(SQL.select('hello world!'))); // => 'hello world'
await db.close();
})();
Полный пример
// example.ts
import {
connect,
SQL,
Decimal,
Uuid,
Connection,
DbType,
outputCommand,
} from "lubejs";
import "lubejs-mssql";
interface Table1 {
id: number;
name: string;
stringField?: string;
floatField?: number;
dateField?: Date;
decimalField?: Decimal;
uuidField?: Uuid;
updatedAt: Date;
binaryField?: ArrayBuffer;
createdAt: Date;
operator?: string;
}
interface Pay {
id?: number;
year: number;
month: number;
amount: Decimal;
personId: number;
}
interface Person {
id?: number;
name: string;
age: number;
}
/**
* Инициализация базы данных
*/
async function initDb(db: Connection) {
await db.query(
SQL.if(SQL.existsTable('table1')).then(SQL.dropTable('table1'))
);
}
``` await db.query(
SQL.createTable("table1").as(({ column }) => [
column("id", DbType.int32).identity().primaryKey(),
column("name", DbType.string(100)).not_null(),
column("string_field", DbType.string(100)).nullable(),
column("float_field", DbType.float).nullable(),
column("date_field", DbType.datetimeoffset).nullable(),
column("decimal_field", DbType.decimal(18, 6)),
column("uuid_field", DbType.uuid),
column("updated_at", DbType.datetimeoffset).default(SQL.now()),
column("binary_field", DbType.binary(DbType.MAX)),
column("created_at", DbType.datetimeoffset).default(SQL.now()),
column("operator", DbType.string(100)).nullable(),
])
);
await db.query(
SQL.if(SQL.exists_table('pay')).then(SQL.drop_table("pay"))
);
await db.query(
SQL.createTable("pay").as(({ column }) => [
column("id", DbType.int32).identity().primary_key(),
column("year", DbType.int32),
column("month", DbType.int32),
column("amount", DbType.decimal(18, 2)),
column("person_id", DbType.int32),
])
);
await db.query(
SQL.if(SQL.exists_table('person')).then(SQL.drop_table("person"))
);
await db.query(
SQL.createTable("person").as(({ column }) => [
column("id", DbType.int32).identity().primary_key(),
column("name", DbType.int32).not_null(), // Ошибка в типе данных, должно быть DbType.string(100)
column("age", DbType.int32),
])
);
}```markdown
/**
* Декларация таблицы Table1
*/
// Это пример
async function example(db: Connection) {
//---------------Вставка данных------------------
/*
* INSERT INTO table1 (stringField, floatField, dateField)
* VALUES ('value1-1', 2, CONVERT(DATETIMEOFFSET, '2019-11-18 00:00:00'))
* , ('value1-2', 1, CONVERT(DATETIMEOFFSET, '2019-11-18 00:00:00'))
* , ('value1-3', 45, CONVERT(DATETIMEOFFSET, '2019-11-18 00:00:00'))
*/
const insertSql = SQL.insert<Table1>("table1").values([
{
name: "item1",
stringField: "value1-1",
floatField: 3.14,
dateField: new Date(),
decimalField: new Decimal("3.1415"),
uuidField: Uuid.new(),
binaryField: Buffer.from('abcdefeg')
},
{
name: "item2",
stringField: "value1-2",
floatField: 1.132,
dateField: new Date(),
decimalField: new Decimal("3.1415"),
uuidField: Uuid.new(),
binaryField: Buffer.from('abcdefeg')
},
{
name: "item3",
stringField: "value1-3",
floatField: 45.2656,
dateField: new Date(),
decimalField: new Decimal("3.1415"),
uuidField: Uuid.new(),
binaryField: Buffer.from('abcdefeg')
},
]);
}
await db.query(insertSql);
```// Также можно использовать следующий способ вставки, эквивалентный вышеописанному методу
await db.insert<Table1>("table1", [
{
name: "item1",
stringField: "значение1-1",
floatField: 3.14,
dateField: new Date(),
decimalField: new Decimal("3.1415"),
uuidField: Uuid.new(),
binaryField: Buffer.from('abcdefeg')
},
{
name: "item2",
stringField: "значение1-2",
floatField: 1.132,
dateField: new Date(),
decimalField: new Decimal("3.1415"),
uuidField: Uuid.new(),
binaryField: Buffer.from('abcdefeg')
},
{
name: "item3",
stringField: "значение1-3",
floatField: 45.2656,
dateField: new Date(),
decimalField: new Decimal("3.1415"),
uuidField: Uuid.new(),
binaryField: Buffer.from('abcdefeg')
},
]);
//----------------Обновление данных------------------
// UPDATE t SET updatedAt = CONVERT(DATETIME, '2019-11-18 00:00:00') FROM table1 t WHERE id = 1
const t = SQL.table<Table1>("table1").as("t");
const updateSql = SQL.update(t)
.set({ updatedAt: new Date(), оператор: "ваше имя" })
.where(t.id.eq(1));
await db.query(updateSql);
// Также можно использовать следующий способ обновления, эквивалентный вышеописанному методу
await db.update<Table1>(
"table1",
{ updatedAt: new Date(), оператор: "ваше имя" },
{ id: 1 }
);
//----------------Удаление данных-------------------
// DELETE t FROM table1 t WHERE t.id = 1
const deleteSql = SQL.delete(t).from(t).where(t.id.eq(1));
await db.query(deleteSql);
// Также можно использовать следующий способ удаления
// DELETE table1 WHERE id = 1
await db.delete("table1", { id: 1 });
//----------------Запрос данных----------------------
// SELECT t.* FROM table1 AS t WHERE t.id = 1 AND t.name = 'name1'
const selectSql = SQL.select(t.star)
.from(t)
.where(SQL.and(t.id.eq(1), t.name.eq("name1")));
console.log((await db.query(selectSql)).rows);```md
// Вы также можете выполнить запрос таким образом
// SELECT * FROM table1 WHERE id = 1 AND name = 'name1'
console.log(await db.select("table1", {
where: {
id: 1,
name: "item1",
},
}));
// ---------------Ниже приведен составной запрос--------------
const p = SQL.table<Person>("person").as("p");
const pay = SQL.table<Pay>("pay");
const sql = SQL.select({
год: pay.год,
месяц: pay.месяц,
имя: p.имя,
возраст: p.возраст,
общая_сумма: SQL.sum(pay.сумма),
})
.from(pay)
.join(p, pay.personId.eq(p.id))
.where(p.возраст.lte(18))
.groupBy(p.имя, p.возраст, pay.год, pay.месяц)
.having(SQL.sum(pay.сумма).gte(new Decimal(100000)))
.orderBy(pay.год.asc(), pay.месяц.asc(), SQL.sum(pay.сумма).asc(), p.возраст.asc())
.offset(20)
.limit(50);
console.log((await db.query(sql)).rows);
```ts
(async () => {
// Создание соединения Lube
const db = await connect("mssql://sa:!crgd-2021@rancher.vm/Test");
// Открытие соединения
await db.open();
// Вывод логов
db.on('command', (cmd) => outputCommand(cmd, process.stdout));
try {
await initDb(db);
await example(db);
} finally {
await db.close();
}
})();
Обратите внимание: lubejs в настоящее время находится в режиме предварительной версии, внутренняя структура может меняться, а общедоступный API может незначительно измениться, но основные изменения не ожидаются.
lubejs/core
— это базовый пакет, который включает средства построения SQL и инструменты выполнения SQL.lubejs
— полный пакет, который включает все содержимое lubejs/core
, а также ORM-функциональность, командную строку для миграций данных и т.д.node-mssql
.Node.js >= 12.0
Все SQL-запросы могут быть созданы с помощью построителя запросов. lubejs
экспортирует глобальный объект-построитель SQL
. Почти все SQL-запросы можно создать с использованием этого объекта, например SQL.select
, SQL.update
, SQL.delete
.
// Импорт объекта SQL
import { SQL } from 'lubejs';
```Для большего удобства вы можете использовать деконструкцию для импорта необходимых ключевых слов SQL:
```ts
const {
insert,
delete: $delete // "delete" является зарезервированным словом в JavaScript, поэтому используется псевдоним "$delete"
} = SQL;
// Создание SQL-запроса для вставки двух записей в таблицу table1
const sql = insert('table1').values([{ name: '张三', age: 19, sex: '男' }, { name: '李四', age: 25, sex: '男' }]);
// Создание SQL-запроса для удаления записи с ID 1 из таблицы table1
const sql = $delete('table1').where({ id: 1 });
Дополнительные примеры использования объекта SQL
представлены в руководстве по API.
Обратите внимание: "delete" является зарезервированным словом в JavaScript, поэтому требуется использование псевдонима.
Чтобы обеспечить лучшую совместимость с различными базами данных, lubejs определяет стандартные функции, которые используются для унификации действий при работе с несколькими базами данных. Эти функции находятся внутри объекта-построителя SQL.
SQL
определяет множество часто используемых функций и операций.
| ---------------------- | ------------------------------------------------------------- | ---------- |
| Преобразование типов | `SQL.convert(expr, dbType)`, `Expression.prototype.to(dbType)`<br>**Пример:**<br>`SQL.convert('abc', DbType.string(100))`<br>`SQL.literal('100').to(DbType.int32)` | |
| Возврат значения по умолчанию при пустоте | `SQL.nvl(value, defaultValue)` | |
```**Агрегирующие функции**
| Описание | Функция | Примечание |
| ------------- | ------------------ | ---------- |
| Подсчет | `SQL.count(expr)` | |
| Среднее значение | `SQL.avg(expr)` | |
| Сумма | `SQL.sum(expr)` | |
| Максимум | `SQL.max(expr)` | |
| Минимум | `SQL.min(expr)` | |**Функции работы с датами**| Описание | Функция | Примечание |
| -------------------------- | ----------------------------------------- | ---------- |
| Текущее время | `SQL.now(expr)` | |
| Время UTC | `SQL.utcNow(expr)` | |
| Переключение часового пояса | `SQL.switchTimezone(date, offset)` | |
| Форматирование даты | `SQL.formatDate(date, format)` | |
| Получение года из даты | `SQL.yearOf(date)` | |
| Получение месяца из даты | `SQL.monthOf(date)` | |
| Получение дня из даты | `SQL.dayOf(date)` | |
| Разница в днях между двумя датами | `SQL.daysBetween(start, end)` | |
| Разница в месяцах между двумя датами | `SQL.monthsBetween(start, end)` | |
| Разница в годах между двумя датами | `SQL.yearsBetween(start, end)` | |
| Разница в часах между двумя датами | `SQL.hoursBetween(start, end)` | |
| Разница в минутах между двумя датами | `SQL.minutesBetween(start, end)` | |
| Разница в секундах между двумя датами | `SQL.secondsBetween(start, end)` | |
| Дата после добавления дней | `SQL.addDays(date, days)` | |
| Дата после добавления месяцев | `SQL.addMonths(date, months)` | |
| Дата после добавления лет | `SQL.addYears(date, years)` | |
| Дата после добавления часов | `SQL.addHours(date, hours)` | |
| Дата после добавления минут | `SQL.addMinutes(date, minutes)` | |
| Дата после добавления секунд | `SQL.addSeconds(date, seconds)` | |
**Строковые функции**| Описание | Функция | Примечание |
| ------------------------------------ | ----------------------------------------- | ---------- |
| Получение количества символов строки | `SQL.strlen(str)` | |
| Получение размера строки в байтах | `SQL.strsize(str)` | |
| Вырезка строки | `SQL.substr(str, start, len)` | |
| Замена строки | `SQL.replace(str, search, text)` | |
| Удаление пробелов с обоих концов | `SQL.trim(str)` | |
| Удаление пробелов справа | `SQL.trimEnd(str)` | |
| Преобразование в нижний регистр | `SQL.lower(str)` | |
| Преобразование в верхний регистр | `SQL.upper(str)` | |
| Получение позиции строки внутри другой строки | `SQL.strpos(str, search, startAt)` | |
| Получение ASCII-кода символа | `SQL.ascii(str)` | |
| Преобразование ASCII-кода в символ | `SQL.asciiChar(code)` | |
| Получение Unicode-кода символа | `SQL.unicode(str)` | |
| Преобразование Unicode-кода в символ | `SQL.codecChar(code)` | |
**Математические функции**| Описание | Функция | Примечание |
| --------- | ------- | ----------- |
| Абсолютное значение | `SQL.abs(value)` | |
| Экспонента | `SQL.exp(value)` | |
| Вверх до целого числа | `SQL.ceil(value)` | |
| Вниз до целого числа | `SQL.floor(value)` | |
| Натуральный логарифм | `SQL.ln(value)` | |
| Логарифм | `SQL.log(value)` | |
| Число π | `SQL.pi()` | |
| Возведение в степень | `SQL.power(value, exponent)` | |
| Перевод градусов в радианы | `SQL.radians(value)` | |
| Перевод радианов в градусы | `SQL.degrees(value)` | |
| Генерация случайного числа | `SQL.random(value)` | |
| Округление до указанной цифры | `SQL.round(value)` | |
| Функция знака | `SQL.sign(value)` | |
| Квадратный корень | `SQL.sqrt(value)` | |
| Косинус | `SQL.cos(value)` | |
| Синус | `SQL.sin(value)` | |
| Тангенс | `SQL.tan(value)` | |
| Арккосинус | `SQL.acos(value)` | |
| Арксинус | `SQL.asin(value)` | |
| Арктангенс | `SQL.atan(value)` | |
| Котангенс | `SQL.cot(value)` | |**Основные операции**|| Описание | Метод | Примечание |
| ------------------------------ | -------------------------------------------------------------------- | ---------- |
| Проверка наличия таблицы | `SQL.existsTable(tableName)` | |
| Проверка отсутствия базы данных | `SQL.existsDatabase(dbName)` | |
| Проверка отсутствия представления | `SQL.existsView(viewName)` | |
| Проверка отсутствия функции | `SQL.existsFunction(functionName)` | |
| Проверка отсутствия хранимой процедуры | `SQL.existsProcedure(procedureName)` | |
| Проверка отсутствия последовательности | `SQL.existsSequence(sequenceName)` | |
| Получение текущей базы данных | `SQL.currentDatabase()` | |
| Получение текущей схемы по умолчанию | `SQL.defaultSchema()` | |
| Получение следующего значения последовательности | `SQL.sequenceNextValue(sequenceName)` | |
### Типы базы данных (DbType)
Для обеспечения совместимости между различными базами данных lubejs определяет промежуточные типы данных DbType. В большинстве случаев рекомендуется не использовать нативные типы данных конкретной базы данных.
#### Использование DbType
```ts
import { DbType, SQL } from 'lubejs';
// Преобразование литерала 1 в boolean
const sql = SQL.select(SQL.literal(1).to(DbType.boolean));
// => SELECT CAST(1 AS bit) AS [#column_1]
```Нативные типы
Использование нативных типов данных базы данных для создания SQL запросов:
```ts
const sql = SQL.createTable('Person').as(builder => {
builder.column('name', DbType.raw('text'));
});
```#### Таблица соответствия типов DbType| Тип | Соответствует JS типу | Соответствует SQL Server типу | Описание |
| -------------------------------- | ---------------------------------- | ------------------------------ | ----------------------- |
| DbType.int8 | Number | tinyint | |
| DbType.int16 | Number | smallint | |
| DbType.int32 | Number | int | |
| DbType.int64 | BigInt (ES2019) | bigint | |
| DbType.decimal(precision, digit) | Decimal (из библиотеки `decimal.js`) | decimal(precision, digit) | |
| DbType.float32 | Number | float | |
| DbType.float64 | Number | real | |
| DbType.string(length) | String | nvarchar(length) | |
| DbType.date | Date | date | |
| DbType.datetime | Date | datetime | |
| DbType.datetimeoffset | Date | datetimeoffset | |
| DbType.time | Time (встроен в lubejs) | time | Дата |
| DbType.binary | ArrayBuffer/Buffer | varbinary(x) | |
| DbType.boolean | Boolean | bit | |
| DbType.uuid | Uuid (встроен в lubejs, основан на библиотеке `uuid`) | UNIQUEIDENTIFIER | |rowflag | ArrayBuffer | TIMESTAMP | | json | Объект или `custom type` | nvarchar(max) | Сохраняется в MSSQL как JSON-строка |
| DbType.list(dbType) | Array<dbType> | nvarchar(max) | Сохраняется в MSSQL как JSON-строка |### Набор строк (Rowset)|Все объекты, доступные через `SELECT FROM`, могут называться наборами строк. В частности:
- Таблицы/представления
- Алиасы подзапросов SELECT
- Элементы запроса с использованием WITH
- Возвращаемое значение таблицной функции
- Таблиценные переменные
**Объявление таблиц/представлений**
```ts
const personTable = SQL.table<Person>('Person');
const houseTable = SQL.table<House>('House')
Добавление алиаса для таблиц
const p = SQL.table<Person>('Person').as('p');
const h = SQL.table<House>('House').as('h');
Доступ к полям таблиц
const sql = SQL.select(p.name).from(p);
// => SELECT p.name FROM Person p
Добавление алиаса для полей
const sql = SQL.select(p.name.as('first_name')).from(p);
// => SELECT p.name AS first_name FROM Person p
Примечание: Объявление объектов таблиц не является обязательным, но если не использовать объекты таблиц, то при каждом использовании таблицы требуется явное указание типа (например: SQL.select<Person>(SQL.star).from('Person')
). В противном случае оператор SELECT будет лишён возвращаемого типа и вместо него будет использован тип any
.
В lubejs все объекты выражений наследуются от класса Expression
. Это включает следующие компоненты:
В большинстве случаев можно использовать значения JavaScript для передачи литерала, как показано ниже:```ts SQL.update(p).set({ name: 'Имя' }) // => UPDATE p SET name = 'Имя'
Здесь `'Имя'` представляет собой строковый литерал, который `lubejs` автоматически распознаёт как структурированный литерал. Однако, когда требуется использовать литерал в вычислениях, поскольку значения JavaScript не имеют таких методов, можно создать литерал SQL с помощью `SQL.literal(1)`.
```ts
// Поиск людей старше 18 лет
const sql = SQL.select(p.star).from(p).where(SQL.literal(18).lt(p.age));
// => SELECT p.* FROM Person p WHERE 18 < p.age
Объявление переменной
const name = SQL.var('name', DbType.string);
// DECLARE @name NVARCHAR(MAX)
name.set('Иванов')
// SET @name = 'Иванов';
Объявление и одновременное присвоение значений
const name = SQL.var('name', 'Петров');
// DECLARE @name NVARCHAR(MAX)
// SET @name = N'Петров'
Использование переменной в запросах
const sql = SQL.select(p.star).from(p).where(p.name.eq(name))
// => SELECT p.* FROM Person p WHERE p.name = @name
См. Вызов функции
| -------- | ---------------------------------------------------------------------------------------------- | -------------- |
| + | `SQL.add(left, right)`、 `Expression.prototype.add(value)` | Сложение |
| - | `SQL.sub(left, right)`、 `Expression.prototype.sub(value)` | Вычитание |
| * | `SQL.mul(left, right)`、 `Expression.prototype.mul(value)` | Умножение |
| / | `SQL.div(left, right)`、 `Expression.prototype.div(value)` | Деление |
| +(mssql) | `SQL.concat(left, right)`、 `Expression.prototype.concat(value)` | Конкатенация |
| %(mssql) | `SQL.mod(left, right)`、 `Expression.prototype.mod(value)` | Взятие остатка|
| &(mssql) | `SQL.and(left, right)`、 `Expression.prototype.and(value)` | Логическое И |
| \|(mssql)| `SQL.or(left, right)`、 `Expression.prototype.or(value)` | Логическое ИЛИ|
| ^(mssql) | `SQL.xor(left, right)`、 `Expression.prototype.xor(value)` | Исключающее ИЛИ|
| >>(mssql)| `SQL.shr(left, right)`、 `Expression.prototype.shr(value)` | Сдвиг вправо |
| <<(mssql)| `SQL.shl(left, right)`、 `Expression.prototype.shl(value)` | Сдвиг влево |
| XOR(mssql)| `SQL.xor(left, right)`、 `Expression.prototype.xor(value)` | Исключающее ИЛИ|#### Унарные операторы
| Оператор | Метод | Описание |
| -------- | ------------------------------- | -------------------- |
| - | `SQL.neg(expr)` | Отрицательное число |
| ~ (mssql)| `SQL.not(expr)` | Логическое НЕ |
#### Операции с выражениями
Используя выражения можно легко выполнять арифметические и сравнительные операции, например:
- Сложение
```ts
// Сложение
p.age.add(1)
// => p.age + 1
// Генерация условий для запроса
p.age.lte(18)
// => p.age <= 18
Дополнительные детали см. в разделе Условия WHERE
Условие в JSON формате
Формат JSON используется только для простых условий =
и in
для одиночных таблиц, соединяемых логическим И, обычно для быстрого поиска объекта.
const sql = SQL.select(p.star).from(p).where({ name: '张三', age: [18, 19, 20] })
// => SELECT p.* FROM Person p WHERE p.name = '张三' AND p.age IN (18, 19, 20)
Для использования более сложных условий запроса продолжайте чтение.
Сравнительные условия
Вышеупомянутый SQL-запрос можно записать следующим образом:
const sql = SQL.select(p.star).from(p).where(
p.name.eq('张三').and(p.age.in(18, 19, 20))
)
// => SELECT p.* FROM Person p WHERE p.name = '张三' AND p.age IN (18, 19, 20)
Операторы сравнения| Оператор | Метод | Описание |
| -------- | ------------------------------------------------------------------------------------------ | -----------------|
| = | SQL.eq(left, right)
、Expression.prototype.eq(value)
| равно |
| <> | SQL.neq(left, right)
、Expression.prototype.neq(value)
| не равно |
| < | SQL.lt(left, right)
、Expression.prototype.lt(value)
| меньше |
| > | SQL.gt(left, right)
、Expression.prototype.gt(value)
| больше |
| <= | SQL.lte(left, right)
、Expression.prototype.lte(value)
| меньше или равно |
| >= | SQL.gte(left, right)
、Expression.prototype.gte(value)
| больше или равно |
| LIKE | SQL.like(left, right)
、Expression.prototype.like(value)
| похожее значение |
| NOT LIKE | SQL.notLike(left, right)
、Expression.prototype.notLike(value)
| не похожее значение |
| IN | SQL.in(left, right)
、Expression.prototype.in(value)
| входит в список |
| NOT IN | SQL.notIn(left, right)
、Expression.prototype.notIn(value)
| не входит в список |Логические условия
// AND-запрос
const sql = SQL.select(p.star).from(p).where(
p.age.lte(18)
.and(p.sex.eq('男'))
.and(p.name.like('张%'))
)
// => SELECT p.* FROM Person p WHERE p.age >= 18 AND p.sex = '男' AND p.name like '张%'
// OR-запрос
const sql = SQL.select(p.star).from(p).where(p.sex.eq('女').or(p.sex.eq('男')))
// => SELECT p.* FROM Person p WHERE p.sex = '女' OR p.sex = '男'
Множество условий соединяются с помощью and
, что позволяет использовать SQL.and
для повышения читаемости:
// Поиск мужчин фамилии Zhang старше 18 лет
const sql = SQL.select(p.star).from(p).where(
SQL.and(
p.age.lte(18),
p.sex.eq('男'),
p.name.like('张%')
)
)
// => SELECT p.* FROM Person p WHERE (p.age >= 18 AND p.sex = '男' AND p.name like '张%')
Примечание: Возвращаемое условие запроса, построенного с помощью SQL.and/SQL.or, является группирующим условием
Логические операторы
Оператор | Метод | Описание |
---|---|---|
AND |
SQL.and(...условия) , Condition.prototype.and(условие)
|
Логическое И |
OR |
SQL.or(...условия) , Condition.prototype.or(условие)
|
Логическое ИЛИ |
NOT | SQL.not(условие) |
Отрицание |
Группирующие условия
//
const sql = SQL.select(p.star).from(p).where(
SQL.and(
p.пол.eq('мужской'),
p.имя.like('Чжан%'),
SQL.group(
p.возраст.lt('18').or(p.возраст.gte('60'))
)
)
)
// => SELECT p.* FROM Person p
// WHERE (p.пол = 'мужской' AND p.имя LIKE 'Чжан%' AND (p.возраст < 18 OR p.возраст >= 60))
```**Условие EXISTS**
```ts
// Поиск людей, имеющих дом
const sql = SQL.select(h.id).from(h).where(
SQL.exists(SQL.select(p.id).from(p).where(p.houseId.eq(h.id)))
)
// => SELECT h.id FROM House h WHERE EXISTS(SELECT p.id FROM Person p WHERE p.houseId = h.id)
В этом разделе мы рассмотрим, как использовать lubejs
, чтобы создать SQL-запросы. Обратите внимание, что в данном разделе рассматривается только построение SQL-запросов, а выполнение этих запросов будет подробно объяснено в последующих разделах.
interface Person {
// Поскольку это автоинкрементируемый столбец, он может быть пустым
id?: number;
имя: string;
возраст?: number;
пол?: 'мужской' | 'женский';
описание?: string;
}
interface Дом {
id?: number;
заголовок: string;
местоположение?: string;
описание?: string;
}
const p = SQL.table<Person>('Person').as('p');
const h = SQL.table<Дом>('Дом').as('h');
Ниже приведен пример полного запроса SELECT:
const sql = SQL.select({
имя: p.имя,
возраст: p.возраст
}).from(p).where(p.id.eq(1))
// => SELECT p.имя AS имя, p.возраст AS возраст FROM Person p WHERE p.id = 1
Формат JSON
const sql = SQL.select({
имя: p.имя,
возраст: p.возраст
}).from(p).where(p.id.eq(1))
// => SELECT p.имя AS имя, p.возраст AS возраст FROM Person p WHERE p.id = 1
Созданный таким образом SQL-запрос будет содержать тип данных, который также будет использоваться при получении результатов запроса. Полное возвращение таблицы (используя звездочку)```ts const sql = SQL.select(p.star).from(p) // => SELECT p.* FROM Person p
Использование `p.star` позволяет вернуть все столбцы таблицы `p`, что будет передано методом `select`.
#### Условия WHERE
Для получения информации о условиях запроса обратитесь к разделу [Условия (Conditions)](#Условия-(Conditions)).
#### Запросы с соединением нескольких таблиц
**Запрос с несколькими таблицами**
```ts
const sql = SQL.select({
personName: p.name,
houseTitle: h.houseTitle
})
.from(p, h)
.where(h.personId.eq(p.id))
// => SELECT p.name AS personName, h.title AS houseTitle FROM Person p, House h
// WHERE h.personId = p.id
Запросы с INNER JOIN / LEFT OUTER JOIN
Внутреннее соединение таблиц
// Запрос для получения данных о домах, связанных с человеком
const sql = SQL.select({
personName: p.name,
houseTitle: h.houseTitle
})
.from(p)
.join(h, h.personId.eq(p.id))
// => SELECT p.name AS personName, h.title AS houseTitle
// FROM Person p INNER JOIN House h ON h.personId = p.id
Левое внешнее соединение таблиц
// Запрос для получения данных о домах, связанных с человеком
const sql = SQL.select({
personName: p.name,
houseTitle: h.houseTitle
})
.from(p)
.leftJoin(h, h.personId.eq(p.id))
// => SELECT p.name AS personName, h.title AS houseTitle
// FROM Person p LEFT OUTER JOIN House h ON h.personId = p.id
Примечание: В связи с правилами использования SQL, поддерживаются только соединения типа INNER JOIN
и LEFT OUTER JOIN
. Соединения типа RIGHT JOIN
не поддерживаются.
Вложенные запросы
// Запрос для получения количества домов, связанных с человеком
const sql = SQL.select({
personName: p.name,
houseCount: SQL.select(SQL.count(h.id)).from(h).where(h.personId.eq(p.id))
})
.from(p)
// => SELECT
// p.name AS personName,
// (SELECT count(h.id) FROM House h WHERE h.personId = p.id) AS houseCount
// FROM Person p
```**Алгоритмическое имя для подзапроса**
По сравнению с нативным SQL, синтаксис LubeJS более чистый и удобочитаемый.
```ts
const hc = SQL.select({
personId: h.personId,
houseCount: SQL.count(h.id)
}).from(h).groupBy(h.personId).as('hc')
const sql = SQL.select({
personName: p.name,
houseCount: hc.houseCount
})
.from(p)
.join(hc, hc.personId.eq(p.id))
// => SELECT p.name AS personName, hc.houseCount as houseCount
// FROM Person p
// JOIN (SELECT personId, count(h.id) AS houseCount FROM h GROUP BY h.personId) as hc
Подзапрос IN
const sql = SQL.select(h.star).from(h).where(h.personId.in(
SQL.select(p.id).from(p).where(p.name.like('张%'))
))
// => SELECT h.* FROM House h WHERE h.personId IN (SELECT p.id FROM Person p WHERE p.name LIKE '张%')
GROUP BY
// Подсчет количества домов для каждого человека
const sql = SQL.select({
personName: p.name,
houseCount: SQL.count(h.id)
})
.from(p)
.join(h, h.personId.eq(p.id))
.groupBy(p.name)
// => SELECT p.name AS personName, COUNT(h.id) AS houseCount
// FROM Person p JOIN House h ON h.personId = p.id
// GROUP BY p.name
Использование предложения HAVING
// Поиск людей с более чем двумя домами
const sql = SQL.select({
personName: p.name,
houseCount: SQL.count(h.id)
})
.from(p)
.join(h, h.personId.eq(p.id))
.where(h.location.eq('Гуанчжоу'))
.groupBy(p.name)
.having(SQL.count(h.id).gte(2))
// => SELECT p.name AS personName, COUNT(h.id) AS houseCount
// FROM Person p JOIN House h ON h.personId = p.id
// WHERE h.location = 'Гуанчжоу'
// GROUP BY p.name
// HAVING COUNT(h.id) >= 2
const adult = SQL.select(p.star).from(p).where(p.age.gte('18')).asWith('adult')
const sql = SQL.with(adult).select(a.star).from(adult.as('a'))
// => WITH adult as (SELECT p.* FROM Person p WHERE p.age >= 18)
// SELECT a.* FROM adult a
```### Создание вставочных запросов
#### Одиночная вставка
```typescript
// Одиночная вставка
const sql = SQL.insert(personTable).values({
name: 'Чжан Сань',
age: 23,
sex: 'мужской',
description: 'Это может быть лишним'
});
// => INSERT INTO Person(name, age, sex, description) VALUES ('Чжан Сань', 23, 'мужской', 'Это может быть лишним')
// Множественная вставка
const sql = SQL.insert(personTable).values([
{
name: 'Чжан Сань',
age: 23,
sex: 'мужской',
description: 'Это может быть лишним'
},
{
name: 'Ли Сзи',
age: 43,
sex: 'мужской',
description: 'Это может быть лишним'
}
]);
// => INSERT INTO Person(name, age, sex, description)
// VALUES ('Чжан Сань', 23, 'мужской', 'Это может быть лишним'),
// ('Ли Сзи', 43, 'мужской', 'Это может быть лишним')
Примечание: имя таблицы также можно использовать псевдоним, но псевдоним не будет преобразован в SQL, а вместо этого будет использоваться имя таблицы.
const sql = SQL.update(personTable).set({
age: personTable.age.add(1) // Возраст увеличен на один год
}).where(personTable.name.eq('Зhang San'))
// => UPDATE Person SET age = p.age + 1 WHERE Person.name = 'Зhang San'
// Эти люди, у которых есть недвижимость в Гуанчжоу, стали старше на один год
const sql = SQL.update(p).set({
age: p.age.add(1) // Возраст увеличен на один год
})
.from(p)
.join(h, h.personId.eq(p.id))
.where(h.location.eq('Гуанчжоу'))
```// => UPDATE p SET age = p.age + 1
// FROM Person p
// JOIN House h ON h.personId = p.id
// WHERE h.location = 'Гуанчжоу'
### Создание запроса DELETE
#### Удаление с использованием имени таблицы
```ts
const sql = SQL.delete(personTable).where(personTable.age.gt(60));
// => DELETE Person WHERE Person.age > 60
// Удаление данных людей, имеющих недвижимость в Гуанчжоу
const sql = SQL.delete(p)
.from(p)
.join(h, h.personId.eq(p.id))
.where(h.location.eq('Гуанчжоу'));
// => DELETE p
// FROM Person p
// JOIN House h ON h.personId = p.id
// WHERE h.location = 'Гуанчжоу'
Предположим, что у нас есть скалярная функция с именем Hello
, которая возвращает тип varchar(100)
и имеет один параметр @name varchar(50)
. Она может быть вызвана следующим образом:
SQL.select(SQL.func('Hello').invokeAsScalar('мир')); // => SELECT hello('мир')
Можно также объявить функцию как JavaScript-функцию для более полной проверки типов и повторного использования.
// Объявление функции
const hello = SQL.makeInvoke<string, ['name']: string>('Hello');
// Вызов функции
const sql = SQL.select(hello('мир')); // => SELECT hello('мир')
Предположим, что у нас есть табличная функция с именем get_person
, которая возвращает тип Person
и имеет один параметр @name varchar(50)
. Она может быть вызвана следующим образом:
const p = SQL.func('get_person').invokeAsPerson<Person>('Зhang San').as(p);
const sql = SQL.select(p.star).from(p); // => SELECT p.* FROM get_person('Зhang San') AS p
```#### Декларативный вызов как JavaScript-функция (табличная функция)
```ts
// Объявление функции
const getPerson = SQL.makeInvoke<Person, [['name']: string]>('get_person');
const p = getPerson('Чжан Сан').as('p');
// Вызов функции
const sql = SQL.select(p.star).from(p)); // => SELECT p.* FROM get_person('Чжан Сан') AS p
Предположим, что у нас есть следующая хранимая процедура:
CREATE PROCEDURE sp_get_person
(
@type VARCHAR(20) = 'all',
@total INT OUTPUT
)
AS
BEGIN
SELECT @total = COUNT(p.id) FROM Person;
IF (@type = 'adult')
SELECT * FROM Person p WHERE p.age >= 18;
ELSE IF (@type = 'children')
SELECT * FROM Person p WHERE p.age < 18;
ELSE IF (@type = 'aged')
SELECT * FROM Person p WHERE p.age >= 50;
ELSE
SELECT * FROM Person;
END
RETURN 100;
END
const sql = SQL.execute<number, Person>('sp_get_person', ['children', Yöntem]);
const params: Parameter[] = [
SQL.input('type', 'adult'),
SQL.output('total', DbType.int32)
];
const sql = SQL.execute<number, Person>('sp_get_person', ['children', 0]);
const result = await db.query(sql);
console.log(result.returnValue); // => 100
console.log(result.rows); // Результат выполнения SELECT запроса
console.log(result.output.total); // => Количество строк в таблице Person
Можно также объявить хранимую процедуру как JavaScript функцию, чтобы иметь более полную проверку типов и повторное использование.
// Объявление SQL хранимой процедуры как TypeScript функции
const sp_get_person = SQL.makeExec<'adult' | 'children' | 'aged' | 'all', number, [Person]>('sp_get_person');
// Затем мы можем вызвать её таким образом, чтобы достичь того же эффекта, что и быстрый вызов.
const sql = sp_get_person('children', 0);
```### Использование строк SQL для построения запросов
Обычно мы не рекомендуем использовать этот метод для построения SQL, так как это приведёт к отключению проверки типов TypeScript. Однако при некоторых специфических случаях, когда нам может потребоваться использовать конкретный подход, можно создать SQL запрос следующим образом:
```ts
SQL.raw('')
const sql = SQL.createTable('Person').as(({ column }) => [
// Определяющая колонка
column('id', DbType.int32).notNull().primaryKey().identity(),
column('name', DbType.string(100)).notNull(),
column('age', DbType.int32).null(),
column('sex', DbType.string(2)).null(),
column('description', DbType.string(100)).null(),
// По умолчанию значение getDate()
column('createDate', DbType.datetime).default(SQL.now())
])
// => CREATE TABLE Person(
// id int not null primary key identity(1, 1),
// name nvarchar(100) not null,
// age int null,
// sex nvarchar(2) null,
// description nvarchar(100) null,
// createDate datetime default (sysdate())
// )
const sql = SQL.alterTable('Person').addColumn(column => column('rowflag', DbType.rowflag).notNull())
// => ALTER TABLE Person add column rowflag TIMESTAMP NOT NULL
const sql = SQL.alterTable('Person').dropColumn('rowflag')
// => ALTER TABLE Person drop column rowflag
const sql = SQL.alterTable('House').addForeignKey(
fk => fk('FK_HOUSE_PERSON').on('personId').reference('Person', ['id'])
)
// => ALTER TABLE House ADD FOREIGN KEY FK_HOUSE_PERSON ON personId REFERENCES Person(id)
const sql = SQL.alterTable('House').dropForeignKey('FK_HOUSE_PERSON')
```#### Создание индекса
```ts
const sql = SQL.createIndex('IX_Person_name').on('Person', ['name']);
// => CREATE INDEX IX_Person_name ON Person(name)
const sql = SQL.dropIndex('Person', 'IX_Person_name');
// => DROP INDEX IX_Person_name
Примечание: из-за различий в поведении различных диалектов баз данных, здесь требуется передача имени таблицы.
Из-за ограничений по объему, в этом разделе больше нет информации о других методах использования запросов. Для получения более подробной информации обратитесь к "Руководству по API".
| ---------------------------------- | ------------------------------------------------------------ | --------------------------- |
| insert | SQL.insert(...).values(...)
| |
| update | SQL.update(...).set(...).from(...).where(...)
| |
| select | SQL.select(...).from(...).where(...)
Дополнительные продвинутые методы см. в разделе Создание инструкций SELECT | |
| delete | SQL.delete(...).from(...).where(...)
| |
| case when ... then ... else ... end | SQL.case(...).when(...).else(...)
| Инструкция CASE |
| execute | SQL.execute(...)
、SQL.proc(...).execute(...)
| Вызов хранимых процедур |
| | SQL.makeExec(...)
| Объявление SQL-процедуры как JS-функции |
| invoke | SQL.invokeAsScalar(...)
,SQL.invokeAsTable(...)
| Вызов функций |
| | SQL.makeInvoke(...)
| Объявление SQL-функций как JS-функций |Примечание: В таблице используются символы кавычек “”
, которые могут быть заменены на обычные "
.#### Операции с данными| Инструкция | Использование | Описание |
| ---------------------- | --------------------------------------------------------------------- | -------- |
| create table | SQL.createTable(...).as(...)
. Дополнительные продвинутые методы см. в «Создание таблиц» | |
| alter table | SQL.alterTable(...).as(...)
. Дополнительные продвинутые методы см. в «Изменение таблиц» | |
| drop table | SQL.dropTable(...)
| |
| create view | SQL.createView(...).as(...)
| |
| alter view | SQL.alterView(...).as(...)
| |
| drop view | SQL.dropView(...)
| |
| create procedure | SQL.createProcedure(...).as(...)
| |
| alter procedure | SQL.alterProcedure(...).as(...)
| |
| drop procedure | SQL.dropProcedure(...)
| |
| create function | SQL.createFunction(...).as(...)
| |
| alter function | SQL.alterFunction(...).as(...)
| |
| drop function | SQL.dropFunction(...)
| |
| create sequence | SQL.createSequence(...).as(...).startWith(...).incrementBy(...)
| |
| drop sequence | SQL.dropSequence(...)
| |
| create database | SQL.createDatabase(...).collate(...)
| |
| alter database | SQL.alterDatabase(...).collate(...)
| |
| drop database | SQL.dropDatabase(...)
| |
| create index | SQL.createIndex(...).on(...)
. ) | | | drop index |
SQL.drop_index(...) | |#### Управляющие инструкции программы| Инstrukciya | Ispol'zovanie | Opisanie | | ------------- | ----------------------------------- | -------- | | if..then..else |
SQL.if(...).then(...).else(...)| | | while |
SQL.while(...).do(...) | | | begin ... end |
SQL.block(...) | | | break |
SQL.break() | | | return |
SQL.return(...) | | | continue |
SQL.continue` | |
import { SQL, DbType, connect } from 'lubejs';
(async () => {
const db = await connect('mssql://user:password@localhost:1433');
await db.query(SQL.createDatabase('test-database'));
await db.changeDatabase('test-database');
await db.query(SQL.createTable('Person').as(({ column }) => [
column('id', DbType.int32).notNull().primaryKey().identity(),
column('name', DbType.string(100)).notNull(),
column('age', DbType.int32).null(),
column('sex', DbType.string(2)).null(),
column('description', DbType.string(100)).null()
]));
await db.close();
})();
Класс Connection
является основой всего уровня баз данных и содержит множество методов, более удобных для использования, чем создание SQL (например, insert, update, select, delete и т.д.)
Вы можете использовать следующий синтаксис для создания соединения с базой данных:
const db = await connect('mssql://user:password@localhost:1433/database');
Использование конструкций для создания SQL-запросов
const sql = SQL.select(1);
const result = await db.query(sql);
console.log(result); // => { rows: [{ '#column_1': 1 }] }
Передача параметров вместе с запросом```ts const sql = SQL.select(SQL.input('@p', 1)); const result = await db.query(sql); console.log(result); // => { rows: [{ '#column_1': 1 }] }
Получение выходных параметров
```ts
const p1 = SQL.output('p', DbType.int32);
const sql = SQL.select(SQL.assign(p1, 1));
const result = await db.query(sql); // => SELECT @p = 1;
// Получаем значение из списка выходных значений
console.log(result.output['p']); // => 1
// Также можно получить значение, используя оригинальный параметр
console.log(p1.value); // => 1
Использование сырой SQL-строки запроса
Использование SQL-строки
const sql = 'SELECT 1 AS [#column_1]';
const result = await db.query(sql);
console.log(result); // => { rows: [{ '#column_1': 1 }]} }
Примечание: при использовании сырой SQL-строки запроса, lubejs не будет указывать имя столбца.
Передача параметров вместе с запросом
const sql = 'SELECT @p1 AS [#column_1]';
const result = await db.query(sql, [ 1 ]);
console.log(result); // => { rows: [{ '#column_1': 1 }]} }
Возвращаемое значение:- Тип: QueryResult<T, R, O>
**Запрос одного значения**
```ts
const sql = SQL.select(1);
const result = await db.queryScalar(sql);
console.log(result); // => 1
Использование параметров аналогично передаче параметров методом .query
.
Запрос значения с использованием выражения
Можно также запросить значение, используя выражение; lubejs автоматически преобразует это в SELECT-запрос и возвращает первое значение.
const result = await db.queryScalar(SQL.literal(1));
console.log(result); // => 1
Использование объекта таблицы для запроса
await db.insert(personTable, {
name: '张三',
age: 42,
sex: 'мужской',
description: 'это можно игнорировать'
});
Прямое использование имени таблицы для вставки
await db.insert<Person>('Person', {
name: '张三',
age: 42,
sex: 'мужской',
description: 'это можно игнорировать'
});
Внимание: при прямой вставке с указанием имени таблицы и отсутствием спецификации типа generics, потеряется типизация.
Поиск одной записи:
const row = await db.find(personTable, { name: '张三' });
console.log(row);
// => {
// id: 1,
// name: '张三',
// age: 42,
// sex: 'мужской',
// description: 'это можно игнорировать'
// }
const rows = await db.select(personTable, {
where: {
name: '张三'
}
});
console.log(rows);
// => [{
// id: 1,
// name: '张三',
// age: 42,
// sex: 'мужской',
// description: 'это можно игнорировать'
// }]
Указание полей для возврата
const rows = await db.select(personTable, {
fields: ['name'],
where: {
name: '张三'
}
});
``````md
## Вывод результатов запроса
```javascript
console.log(rows);
// => [{
// name: 'Зhang San'
// }]
await db.delete<Person>('Person', {
id: 1
});
await db.update<Person>('Person', {
age: 43 // стал старше на год
}, { id: 1 });
Если вам требуется знать, какие действия выполняются соединением, вы можете использовать следующий метод для вывода лога:
db.on('command', cmd => {
//
console.log('SQL: ' + cmd.sql);
// параметры, переданные при выполнении запроса
console.log('ПАРАМЕТРЫ: ' + JSON.stringify(cmd.params));
})
Многие ситуации требуют того, чтобы строки подключения или конфигурации не были записаны в коде, либо требуется использование инструмента миграции баз данных. В таких случаях можно использовать конфигурационный файл. Для lubejs
имя конфигурационного файла должно быть .lubejs.ts
/ .lubejs.js
. Из-за необходимости импорта драйверов и других причин, использование JSON-формата для конфигураций не поддерживается.
Структура конфигурационного файла:
// Импорт типа конфигурации, в JavaScript этот шаг можно пропустить
import { LubeConfig } from 'lubejs';
// Импорт драйвера
import 'lubejs-mssql';
import './orm-configure'
// import 'orm';
``````javascript
export const config: LubeConfig = {
// Название по умолчанию для конфигурации, используется при вызове функции connect для создания соединения, если параметры не передаются. Название должно совпадать с одним из существующих узлов в `configures`.
default: 'lubejs-test',
// Директория для хранения файлов миграции, используемых при миграциях данных. По умолчанию это `migrates`.
migrateDir: 'migrates',
// Конфигурации
configures: {
// Название конфигурации, когда название совпадает с названием DbContext, эта конфигурация становится основной конфигурацией подключения для этого DbContext.
'lubejs-test': {
// Имя драйвера, которое должно быть заранее импортировано
dialect: 'mssql',
// Название сервера базы данных, также может быть IP-адресом
host: 'your-server',
// Имя пользователя
user: 'sa',
// Пароль
password: 'your!password',
// Название базы данных
database: 'lubejs-orm-test',
port: 1433,
}
}
};
``````typescript
export default config;
Когда название конфигурации совпадает с названием DbContext
, эта конфигурация становится основной конфигурацией подключения для данного DbContext
.
ORM lubejs аналогична стандартному моделированию ORM, но также включает некоторые особенности, такие как глобальный тип ключа.
Для усиления управления типами lubejs определяет правила для типов ключей:
Один объект имеет и только один атрибут, который является ключом
lubejs получает тип этого атрибута через интерфейс EntityKey
, используя его как тип ключа для проверки типов методов, таких как Repository.prototype.get
. Поэтому объекты должны реализовать интерфейс EntityKey
.
По умолчанию, EntityKey
представляет собой пустой интерфейс, поэтому тип ключа будет Scalar
, если интерфейс EntityKey
не определён. При этом вызов Repository.prototype.get
будет выглядеть так:
await repo.get(1);
await repo.get('1');
await repo.get(new Date());
// Все эти вызовы проходят проверку типов
Можно заметить, что мы будем терять более точную проверку типов.
Поэтому перед объявлением сущностей можно использовать возможность объединения типов в TypeScript для определения интерфейса EntityKey
.
// Объявление глобального ключа сущности
declare module 'lubejs' {
export interface EntityKey {
// Если это автоматически генерируемый ID, его следует объявить как nullable, чтобы избежать ошибок синтаксического анализа при использовании .insert.
id?: number;
}
}
```Если нам не требуется повторное определение ключа сущности в каждом объекте, мы можем также использовать следующий подход для неявного объявления ключа сущности для всех сущностей:
```ts
contextBuilder.hasGlobalKey('id', Number);
Сущность является базовым элементом операций ORM lubejs с базой данных. Если нам требуется выполнять операции с базой данных через Repository, то сначала необходимо определить сущностные классы.
Существуют различные виды сущностных классов:
Все пользовательские сущностные классы могут наследовать класс Entity
, либо не наследовать его. Однако сущность должна быть классом, так как интерфейсы и типы в TypeScript будут удалены после компиляции, а декораторы не могут применяться к интерфейсам. Кроме того: сущности, не наследующие класс Entity
, не имеют статического метода .create
.
В последующих разделах будет рассказано, как создавать сущности.
Сущностные классы могут объявлять навигационные свойства, которые предоставляют удобные возможности для выполнения связанных запросов между таблицами.Например, если мы объявляем одно-к-одному (основное) навигационное свойство для сущностного класса User, то используем следующий код для получения данных User вместе с Employee:
const user = await userRepo.get(1, { includes: { employee: true } });
// => { id: 1, ..., employee: { userId: 1, ... }}
При создании сущностных классов и использовании навигационных свойств, они обладают следующими характеристиками:
Идентификатор внешнего ключа
Свойство, которое ссылается на первичный ключ другого объекта в рамках одного сущностного класса, называется идентификатором внешнего ключа.
Например, свойство userId
в сущности Employee
, которое ссылается на первичный ключ таблицы User
, то есть на свойство id
. Таким образом, userId
является идентификатором внешнего ключа.#### Неявные свойства
Навигационные свойства и идентификаторы внешних ключей могут быть объявлены как неявные, то есть они не объявляются явно в классе сущности, но автоматически объявляются моделями. При запросах неявные идентификаторы внешних ключей будут возвращаться вместе со сущностью, тогда как неявные навигационные свойства требуют специальных методов для обхода проверок синтаксиса TypeScript. В возвращаемых данных неявные навигационные свойства и неявные идентификаторы внешних ключей являются недоступными для перечисления (через Object.keys(obj)
эти свойства не вернутся), при сериализации через JSON.stringify(obj)
данные этих свойств также не будут сериализованы.
Неявные навигационные свойства имеют следующие правила названий:
Когда навигационное свойство представляет собой одиночную ссылку на объект, используется имя таблицы (например, Employee
), начальное маленькое буквы. Если такое же название уже существует в сущности (например, employee
), то используется это существующее свойство.
Когда навигационное свойство представляет собой список ссылок (например, один ко многим, многие ко многим), используется имя таблицы (например, Employee
), начальное маленькое буквы, затем преобразуется во множественное число (например, employees
). Если такое же название уже существует в сущности, то используется это существующее свойство.Неявные идентификаторы внешних ключей имеют следующие правила названий:
Используется имя таблицы (в данном случае User
), начальная маленькая буква, затем добавляется суффикс Id
для создания имени идентификатора внешнего ключа. Если такое же название уже существует в сущности, то используется это существующее свойство.
Примечание: Независимо от того, что навигационные свойства или идентификаторы внешних ключей объявлены автоматически, если такие же свойства уже существуют в сущности, модель будет использовать эти существующие свойства, и они больше не будут считаться неявными. Если типы не совпадают, LubeJS не выдаст ошибку (из-за отсутствия функции отражения типа в JS/TS), поэтому следует особенно внимательно относиться к таким объявляемым свойствам и их типам, чтобы избежать проблем. Сильнее всего рекомендуется использовать явные объявления или полностью неявные объявления.
Context
, Repository
или Queryable
возвращаются как экземпляры сущностей (за исключением случаев динамического типа).create
для создания экземпляра сущности, например: User.create({ name: 'abc', password: 'abc123' })
.{}
— конструктор объекта), чтобы работать с базой данных, например: { name: 'abc', password: 'abc123' }
. Однако при использовании Context
для выполнения операций необходимо указывать тип сущности, например: context.insert(User, { name: 'abc', password: 'abc123' })
.#### Контекст базы данных (DbContext)Обычно класс DbContext
соответствует одной базе данных. Объявление Repository
не является обязательным; если его нет, можно прямым образом использовать класс DbContext
для доступа к базе данных.
Для манипулирования данными с помощью DbContext
используется методика, аналогичная той, которая используется для Repository
. Однако требуется наличие экземпляра сущности или указание типа сущности. Подробнее см. объект репозитория (Repository).
Непосредственный поиск данных через DbContext
невозможен; необходимо получить объект для поиска с помощью context.getQueryable()
, затем выполнять поиск.
Можно использовать метод createContext
для создания экземпляра класса DbContext
;
import { createContext } from 'lubejs';
import { DB } from './db';
// Первый аргумент — это конструктор класса DbContext, второй — конфигурация соединения
const ctx = await createContext(DB, 'mssql://user:password@localhost:1433/database');
Создание с использованием файла конфигурации
Когда конфигурация не передается явно, lubejs автоматически использует конфигурацию из файла конфигурации, соответствующую имени класса DbContext. Поэтому следует обратить особое внимание на то, чтобы имя класса DbContext не совпадало с другими.
import { createContext } from 'lubejs';
const ctx = await createContext(DB);
```**Создание экземпляра по умолчанию класса DbContext**
Здесь важно отметить, что по умолчанию класс DbContext может не совпадать с встроенным классом DbContext lubejs. `moduleBuilder` регистрирует первый встреченной класс `DbContext`, который заменяет встроенный класс `DbContext` lubejs. При вызове `createContext` без передачи параметров, lubejs читает конфигурацию соединения из файла конфигурации и создаёт по умолчанию экземпляр класса `DbContext`.
```ts
import { createContext } from 'lubejs';
const ctx = await createContext();
Примечание: перед использованием метода создания с помощью файла конфигурации, необходимо настроить конфигурацию соединения, соответствующую имени класса контекста.
Объект Queryable
реализует все возможности для выполнения запросов и при этом сам является асинхронным итератором, что позволяет использовать его аналогично спискам. API объекта спроектирован так, чтобы отражать методы встроенных массивов JavaScript, поэтому многие операции очень похожи.
Для повышения производительности Queryable
имеет возможность отложенного выполнения запросов, то есть данные будут запрошены из базы данных только при вызовах .fetchAll()
, .fetchFirst()
или при использовании асинхронного итератора for await (const item of userQuerable)
для перебора объекта Queryable
.
const userQueryable = ctx.getQueryable(User);
#### Получение всех данных (fetchAll)
```ts
const allUsers = await userQueryable.fetchAll();
const user = await userQueryable.fetchFirst();
Метод Queryable.prototype.filter
принимает объект Rowset
для построения условий фильтрации. Для фильтрации данных достаточно вернуть объект условия Condition
.
const adminUser = await userQueryable.filter(p => p.name.eq('admin')).fetchFirst();
Метод Queryable.prototype.include
позволяет указывать подэлементы для запроса.
const user = userQueryable.include({
employee: true
}).fetchFirst()
// user => {
// name: '...',
// // ...
// employee: {
// //...
// }
// }
Даже если требуется получить несколько уровней связанных данных, это можно сделать за один запрос.
const user = userQueryable.include({
employee: {
positions: true
}
}).fetchFirst();
// user => {
// name: '...',
// // ...
// employee: {
// //...
// positions: [...]
// }
// }
for await (const item of userQueryable) {
console.log(item);
}
Объект Repository
предназначен для предоставления функциональностей получения, вставки, обновления и сохранения данных.
const userRepo = ctx.getRepository(User);
for await (const item of userRepo) {
console.log(item);
}
```#### Получение данных одного объекта (get)
Передача ключей для получения данных объекта. При попытке получить несуществующие данные будет выброшено исключение. Если вы хотите избежать выбрасывания исключения, используйте метод `Queryable.prototype.filter` для выполнения запроса. **Получение данных с помощью DbContext**
```ts
const user = await ctx.get(User, 1);
// user — это экземпляр класса User
Получение данных с помощью Repository
const user = userRepo.get(1);
// user — это экземпляр класса User, user instanceof User === true
Объект, полученный через .get
, является экземпляром класса-сущности.
При вставке экземпляра сущности, если навигационные свойства имеют значения, будут выполнены операции сохранения объектов этих свойств.
Использование DbContext для вставки
Указание конструктора сущности для вставки:
await ctx.insert(User, {
name: 'admin',
password: '123456'
});
Также можно использовать экземпляр класса сущности для вставки:
await ctx.insert(User.create({
name: 'admin',
password: '123456'
}));
Использование Repository для вставки
Использование JSON-объекта для вставки:
await userRepo.insert({
name: 'admin',
password: '123456'
});
Также можно использовать экземпляр класса сущности для вставки:
await userRepo.insert(User.create({
name: 'admin',
password: '123456'
}));
Указание конструктора сущности для обновления:
const user = await ctx.get(User, 1);
user.password = 'changed!password';
await ctx.update(User, user);
Также можно использовать экземпляр класса сущности для обновления:
// .get возвращает экземпляр класса сущности
const user = await userRepo.get(User, 1);
user.password = 'changed!password';
await userRepo.update(user);
Метод update
обновляет данные, но если таких данных нет в базе данных, будет выброшено исключение.
Использование Repository для обновления
const user = await userRepo.get(1);
user.password = 'changed!password';
await userRepo.update(user);
Метод update
обновляет данные, но если таких данных нет в базе данных, будет выброшено исключение.
Обработка проблем конкурентной работы
Как было упомянуто ранее, при использовании Repository
для сохранения данных существует риск перезаписи данных. Поэтому требуется решение проблемы конкурентной работы. Мы рекомендуем добавить атрибут rowflag
к сущностям, где возможна конкурентная работа. Когда сущность имеет атрибут rowflag
, он автоматически используется как условие для обновления при вызове метода update
. Если данные не могут быть обновлены, будет выброшено исключение.
delete
для сохранения данных навигационных свойств невозможно.Проблемы конкуренции
Для предотвращения перезаписи рекомендуется добавить атрибут rowflag
к классам сущностей, где возможна конкуренция.
Использование DbContext для удаления
Задайте конструктор сущности для удаления:
const user = await ctx.get(User, 1);
await ctx.delete(User, user);
Вы также можете использовать экземпляр сущности для удаления:
const user = await ctx.get(User, 1);
await ctx.delete(user);
Использование Repository для удаления
Задайте конструктор сущности для удаления:
const user = await userRepo.get(1);
await userRepo.delete(user);
Вы также можете использовать экземпляр сущности для удаления:
const user = await userRepo.get(1);
await userRepo.delete(user);
Вы можете удивиться наличию двух методов — .update
и .save
, для сохранения данных. На самом деле, метод .save
предоставляет более продвинутую функциональность. Если существуют связанные свойства и значение этих свойств не равно undefined
, то метод .save
анализирует эти связи и отправляет их вместе с основной записью. В отличие от этого, метод update
обновляет только данные, но не автоматически вставляет или обновляет.
Использование DbContext для сохранения
const user = await ctx.get(User, 1, { includes: { employee: true } });
user.password = 'changed!password';
user.employee.description = 'Сотрудник изменил пароль';
await ctx.save(user);
// Сохраняет как сам объект user, так и его связанный объект employee
```**Использование Repository для сохранения**
```ts
const user = await userRepo.get(1, { includes: { employee: true } });
user.password = 'changed!password';
user.employee.description = 'Сотрудник изменил пароль';
await userRepo.save(user);
// Сохраняет как сам объект user, так и его связанный объект employee
Правила сохранения текущего объекта следующие:
Если данные уже присутствуют в базе данных, выполняется обновление
Если данные отсутствуют в базе данных, выполняется вставкаПравила сохранения связанных свойств следуют:
Когда связанное свойство имеет значение undefined
, никакие действия над этим свойством не выполняются.
Когда связанное свойство представляет собой один-к-одному (основной) отношение:
null
, выполняется операция удаления связанного с ним объекта данных.Когда связанное свойство представляет собой один-к-одному (зависимый) отношение:
null
, выполняется удаление связи (устанавливается внешний ключ как DBNULL
).Когда связанное свойство представляет собой один-ко-многим отношение:
null
или []
, выполняется очистка связанных данных в базе данных.- Когда связанное свойство представляет собой многие ко многим отношениеnull
или []
, выполняется очистка данных в таблице промежуточных связей. Когда связанные свойства имеют многоуровневую структуру, данное правило также применяется.Внимание: при использовании сохранения данных следует учитывать возможность конкурентного доступа; в противном случае существует риск перезаписи данных.
Дополнительные примеры см. в разделе Связывание отношений
Моделирование ORM осуществляется следующими двумя способами:
Оба этих метода объявления можно использовать вместе, причём объявление с помощью декораторов выполняется первым, а объявление с помощью API — вторым, если позволяет конфигурация декораторов.
Внимание: режим декораторов поддерживает только TypeScript и требует открытия опций experimentalDecorators
и emitDecoratorMetadata
в файле tsconfig.json
. В противном случае возникнут ошибки из-за невозможности получения типа свойства.
Объявление сущности User
import { DB } from '../index';
import {
column,
comment,
context,
Entity,
EntityKey,
identity,
key,
nullable,
oneToOne,
principal,
table,
} from 'lubejs';
``````typescript
@comment('Таблица пользователей') // Объявление примечаний, которые будут помещены в расширенные атрибуты базы данных / примечания.
@Table() // Объявление таблицы
@context(() => DB) // Привязка сущности к DbContext
@Data([ // Объявление семенного набора данных, который будет автоматически инициализирован в базе данных при выполнении миграции данных.
{ id: 1, name: 'admin', password: '123456' }
])
export class User extends Entity implements EntityKey {
// Объявление столбца
@Column()
// Объявление ключа
@Key()
// Объявление идентификатора
@Identity()
@Comment('ID')
id?: bigint;
@Comment('Имя пользователя')
@Column()
name!: string;
@Comment('Пароль')
// Объявление пустого значения, если не указано, то значение по умолчанию - null
@Nullable()
@Column()
password?: string;
@Comment('Описание')
@Nullable()
@Column()
description?: string;
}
Вы можете удивиться необходимости использования стрелочной функции при ссылке на класс DB
. На самом деле здесь может возникнуть проблема циклического обращения в Node.js. Когда классы хранятся в отдельных файлах, файл, объявляющий класс DB
, ссылается на файл User
, а файл User
ссылается обратно на класс DB
. Использование функции позволяет отложить выполнение до тех пор, пока все классы не будут созданы, что предотвращает получение undefined
значений при обращении к классам. Аналогичная проблема существует и при взаимной ссылке между сущностями.##### Таблица сущностей (объявление API)
import {
modelBuilder,
DbContext,
Repository,
DbType,
Entity,
SQL,
EntityKey,
Binary,
Decimal,
} from 'lubejs';
/**
* Класс сущности пользователя
*/
export class User extends Entity implements EntityKey {
id?: bigint;
name!: string;
password!: string;
description?: string;
employee?: Employee;
}
modelBuilder.context(DB, context => {
context
// Объявление сущности
.entity(User)
// Преобразование сущности в таблицу
.asTable(table => {
// Добавление комментария к таблице
table.hasComment('Сотрудник');
table
// Объявление столбца
.property(p => p.id, BigInt)
// Объявление столбца как первичного ключа
.isIdentity()
// Добавление комментария к столбцу
.hasComment('ID');
table.property(p => p.name, String).hasComment('Имя сотрудника');
table
.property(p => p.password, String)
// Объявление столбца как nullable
.isNullable()
.hasComment('Пароль');
table
.property(p => p.description, String)
.isNullable()
.hasComment('Описание');
// Объявление первичного ключа
table.hasKey(p => p.id).hasComment('Первичный ключ');
// Объявление начальных данных
table.hasData([{ id: 0, name: 'администратор' }]);
});
});
```
#### Ассоциации
В этом разделе будут рассмотрены примеры сохранения ассоциаций. Для лучшего понимания рекомендуется прочитать [сохранение данных сущностей](#сохранение-сущностей-данных), прежде чем приступить к чтению данного раздела.
##### Один ко одному (основной)
Допустим, что у нас есть две сущности: `User`, `Employee`. Внешний ключ `Employee.userId` ссылается на `User.id`. Объявление сущности `User` выглядит следующим образом:
**Явное объявление навигационного свойства**
Если сущность `Employee` уже имеет объявленное свойство `user`, то можно объявить его следующим образом.
```ts
// # entities/user.ts
// ... остальные импорты
import { Employee } './employee'
@comment('Пользователь') // Объявление комментария, который будет записан в расширенные атрибуты/комментарии базы данных.
@Table() // Объявление сущности как таблицы
@context(() => DB) // Привязка сущности к контексту DbContext
@Data([ // Объявление начальных данных, которые будут автоматически инициализированы при выполнении миграций данных.
{ id: 1, name: 'администратор', password: '123456' }
])
export class User extends Entity implements EntityKey {
// Объявление как столбец
@Column()
// Объявление как первичный ключ
@Key()
// Объявление как автоинкрементируемый столбец
@Identity()
@Comment('ID')
id?: bigint;
```
## Явное объявление внешнего ключа и навигационного свойства
```typescript
// # entities/employee.ts
// Другие импорты...
import { User } from './user';
@table()
@comment('Сотрудник')
@context(() => DB)
export class Employee extends Entity implements EntityKey {
@column()
@key()
@comment('EmployeeID')
@identity()
id?: bigint;
// ... другие свойства
@foreign(() => User, u => u.employee)
@navigation(User)
user?: User;
}
```
### Неявное объявление навигационного свойства
Предположим, что сущность `Employee` не объявила свойство `user`, следующий пример автоматически объявляет неявное **один-к-одному (от)** навигационное свойство `user`.```typescript
// # entities/user.ts
// ... другие импорты
import { Employee } from './employee';
@comment('Пользователь') // Объявление аннотации, которая будет включена в расширенные атрибуты/аннотации базы данных.
@Table() // Объявление таблицы.
@context(() => DB) // Привязка сущности к контексту DbContext.
@Data([ // Объявление семенного набора данных, который будет автоматически инициализирован при выполнении миграций данных.
{ id: 1, name: 'администратор', пароль: '123456' }
])
export class User extends Entity implements EntityKey {
// Объявление колонки
@column()
// Объявление первичного ключа
@key()
// Объявление автоинкрементирующегося поля
@identity()
@comment('ID')
id?: bigint;
// ... другие свойства
@principal() // Объявление этого свойства как основного одно-к-одному отношения.
@oneToOne(() => Employee) // Объявление одно-к-одному отношения.
employee?: Employee;
}
```
### Одно-к-одному отношение
Одно-к-одному отношение также может объявлять навигационное свойство как детальное свойство. После объявления как детального свойства...
#### Получение связанных свойств
```typescript
const user = await userRepo.get(1, { includes: { employee: true } });
// => { id: 1, ..., employee: { userId: 1, ... }}
```
#### Создание и сохранение связанных свойств одновременно
```typescript
const user = User.create({
name: 'администратор',
// ...
employee: {
name: 'Администратор',
description: 'Связь создана'
}
});
await userRepo.insert(user);
// Сохраняет пользователя и сотрудника одновременно
```
### Исправленные строки:
- `пароль` -> пароль
- `администратор` -> администратор
- `Связь создана` -> Связь создана#### Удаление связи
Для удаления связи необходимо выполнить операцию над одно-к-одным (от) сущностью. Конкретные правила сохранения см. в разделе [сохранение данных сущностей (save)](#сохранение-данных-сущностей-save).
##### Одно-к-одному отношение (от)
Продолжим наш предыдущий пример, создаем файл сущности `entities/employee.ts`, **одно-к-одному отношению (от)**.
**Явное объявление внешнего ключа и навигационного свойства**
```typescript
// # entities/employee.ts
// Другие импорты...
import { User } from './user';
@table()
@comment('Сотрудник')
@context(() => DB)
export class Employee extends Entity implements EntityKey {
@column()
@key()
@comment('EmployeeID')
@identity()
id?: bigint;
// ... другие свойства
@foreign(() => User, u => u.employee)
@navigation(User)
user?: User;
}
```
## Неявное объявление внешнего ключа и навигационного свойства
Внешние ключи могут автоматически создаваться моделями без явной декларации, например:
```typescript
// # entities/user.ts
// Другие импорты...
import { Employee } from './employee';
@Table()
@Comment('Пользователь')
@Context(() => DB)
export class User extends Entity implements EntityKey {
@Column()
@Key()
@Identity()
@Comment('UserID')
id?: bigint;
// Другие свойства...
// Объявление одно-к-одного отношения (отношение "одно к одному")
@OneToOne(() => Employee, p => p.user)
@ForeignKey(() => Employee, 'userId') // Объявление внешнего ключа
employee?: Employee | null; // Если userId является nullable, следует указать возможность null, чтобы отключить связь
}
```
### Неявное объявление внешнего ключа и навигационного свойстваВнешний ключ может быть создан автоматически моделью без явной декларации, например:
```typescript
// # entities/сотрудник.ts
// Другие импорты...
import { Пользователь } from './пользователь';
@Table()
@Comment('Сотрудник')
@Контекст(() => БД)
экспортировать класс Сотрудник расширяет Энтити реализует ЭнтитиКлюч {
@Столбец()
@Ключ()
@Comment('ID сотрудника')
@Идентификатор()
id?: bigint;
// Другие свойства...
// Объявление однозначного отношения (отношение "один ко одному" - отношение "деталь")
@ForeignКлюч() // Объявление внешнего ключа
@OneToOne(() => Пользователь) // Объявление однозначного отношения
пользователь?: Пользователь | null; // Если orderId является nullable, следует указать возможность null, чтобы отключить связь
}
```
Предположим, что в сущности `Пользователь` нет объявленного навигационного свойства `сотрудник`, то модель будет автоматически создана это скрытое навигационное свойство (один ко одному - основное) `сотрудник`. В то же время модель также создаст внешний ключ `userId` в сущности `Сотрудник`.
В отношении "один ко одному", правила создания скрытого навигационного свойства следуют ниже:
#### Одновременная вставка данных в однозначное отношение (основное)
```typescript
const сотрудник = Сотрудник.create({
имя: 'Администратор',
// ...
пользователь: {
имя: 'админ',
пароль: '123456'
}
});
await сотрудникRepo.save(сотрудник);
// Одновременно вставлены данные для Сотрудник и Пользователь
```
#### Удаление связанного отношения
```typescript
const сотрудник = await сотрудникRepo.get(1);
сотрудник.пользователь = null;
await сотрудникRepo.save(сотрудник);
```
### Один ко многим отношениеОтношение один ко многим связано с отношением много ко одному.
#### Явное объявление навигационного свойства
Предположим, что сущность `OrderDetail` уже имеет объявленное отношение много ко одному (`order`). Мы можем использовать следующий способ для его связи.
```typescript
// Другие импорты...
import { OrderDetail } from './order-detail';
/**
* Заказ
*/
@Table()
@Context(() => DB)
@Comment('Заказ')
export class Order extends Entity implements EntityKey {
@Column()
@Comment('ID')
@Key()
@Identity()
id?: bigint;
// ...
@OneToMany(() => OrderDetail, p => p.order)
details?: OrderDetail[];
}
```
```## Неявное объявление навигационного свойства
Предположим, что сущность `OrderDetail` не объявила много ко одному навигационное свойство `order`. Моделировщик автоматически создаст неявное много ко одному навигационное свойство `order`.
```ts
// Другие импорты...
import { OrderDetail } from './order-detail'
/**
* Заказ
*/
@Table()
@Context(() => DB)
@Comment('Заказ')
export class Order extends Entity implements EntityKey {
@Column()
@Comment('ID')
@Key()
@Identity()
id?: bigint;
// ...
@OneToMany(() => OrderDetail)
details?: OrderDetail[];
}
```
### Вставка данных в одно ко многим отношение
```ts
const order = Order.create({
orderNo: '202101010001',
// ...
details: [
{
product: 'Карандаш',
count: 1,
price: new Decimal(0.56),
// ...
},
{
product: 'Ручка',
count: 1,
price: new Decimal(10.65),
// ...
},
{
product: 'Блокнот',
count: 1,
price: new Decimal(3.5)
}
]
});
await orderRepo.insert(order);
```### Добавление и удаление деталей заказа
Следующий код удалит запись `OrderDetail`, где продукт — карандаш, и добавит новую запись, где продукт — ручка.
```ts
const order = await orderRepo.get(1, { includes: { detail: true } });
order.details.splice(0, 1); // Удаление первого элемента, то есть карандаша
order.details.push(OrderDetail.create({
product: 'Ручка',
count: 1,
price: new Decimal(1.2),
// ...
}));
await orderRepo.save(order);
```
#### Одно ко многим отношение
##### Явное объявление навигационного свойства
Предположим, что сущность `Order` уже определяет одно ко многим навигационное свойство `details`. Мы можем использовать следующий способ для его ассоциации.
```ts
// ...
import { Order } from './order'
/**
* OrderDetail
*/
@table()
@context(() => DB)
@comment('OrderDetail')
export class OrderDetail extends Entity implements EntityKey {
@column()
@comment('ИД')
@identity()
@key()
id?: bigint;
// ...
@comment('ИД заказа')
@column()
orderId?: bigint;
@foreignKey('orderId') // Указание внешнего ключа
@ManyToOne(() => Order, p => p.details)
order?: Order | null; // Если поле orderId может быть пустым, следует указать возможность значения null для отключения связи
}
```
##### Отключение связи
Следующий код может отключить связь:
```ts
const orderDetail = await orderDetailRepo.get(1);
orderDetail.order = null;
await orderDetailRepo.save(orderDetail);
// => Обновляет orderDetail.orderId значением DBNULL
```
**Неявное объявление навигационных свойств и внешних ключей**Предположим, что сущность `Order` не имеет определённого однозначного отношения один ко многим. Следующий код автоматически создаст неявное навигационное свойство **`orderDetail`**, а также автоматически создаст неявный внешний ключ `orderId` для сущности `Order`.```ts
// ...
import { Order } from './order'
/**
* OrderDetail
*/
@table()
@context(() => DB)
@comment('OrderDetail')
export class OrderDetail extends Entity implements EntityKey {
@column()
@comment('ID')
@identity()
@key()
id?: bigint;
// ...
@comment('OrderId')
@column()
orderId?: bigint;
@manyToOne(() => Order)
order?: Order;
}
```
##### Один ко многим отношение
В реляционной базе данных, отношение один ко многим требует промежуточной таблицы связи, поэтому такому отношению требуется промежуточная сущность связи, которая может быть автоматически создана моделлером или явно создана пользователем.
**Явное объявление навигационных свойств и промежуточной сущности связи**
Сотрудник: entities/employee.ts
```ts
// ...
import { Position } from './position'
@table()
@comment('Employee')
@context(() => DB)
export class Employee extends Entity implements EntityKey {
@column()
@key()
@comment('EmployeeID')
@identity()
id?: bigint;
// ...
@manyToMany(() => Position, p => p.employees) // Навигационное свойство в обратном направлении
positions?: Position[];
@oneToMany(() => EmployeePosition, p => p.employee)
employeePositions?: EmployeePosition[];
}
```
Должность: entities/position.ts
```ts
import { Employee } from './employee'
@table()
@comment('Position')
@context(() => DB)
export class Position extends Entity implements EntityKey {
@column()
@comment('PositionID')
@identity()
@key()
id?: bigint;
// ...
@manyToMany(() => Employee, p => p.positions) // Навигационное свойство в обратном направлении
employees?: Employee[];
@oneToMany(() => EmployeePosition, p => p.position)
employeePositions?: EmployeePosition[];
}
```
Промежуточная сущность связи: entities/employee-position.ts```ts
// ...
import { Position } from './position'
import { Employee } from './employee'
``````markdown
@table()
@context(() => БД)
// @among(() => Позиция, () => Сотрудник, 'position', 'employee')
@among<EmployeePosition, Позиция, Сотрудник>(() => Позиция, () => Сотрудник, п => п.position, п => п.employee)
экспортировать класс EmployeePosition расширяет EntityEngineKey реализует EntityKey {
@column()
@comment('ID')
@key()
@identity()
id?: bigint;
}
```
@comment('PositionID')
@column()
positionId!: bigint;
``` @foreignKey('positionId')
@manyToOne(() => Position, p => p.employeePositions) # Определение навигационной свойства, связанной с одной стороной таблицы
position?: Position;
@column()
@comment('EmployeeID')
employeeId!: bigint;
# Определение навигационной свойства, связанной с внешним ключом таблицы
@foreignKey('employeeId')
@manyToOne(() => Employee, p => p.employeePositions) # Определение навигационной свойства, связанной с другой стороной таблицы
employee?: Employee;
}
```
**Неявное объявление навигационных свойств и промежуточного отношения**
В следующем примере модель автоматически создает следующие элементы:- Навигационное свойство `employees` типа многие ко многим для класса сущностей `Position`.
- Навигационное свойство `employeePositions` типа один ко многим для класса сущностей `Position`, связанное с неявным классом сущностей `EmployeePosition`.
- Навигационное свойство `employeePositions` типа один ко многим для класса сущностей `Employee`, связанное с неявным классом сущностей `EmployeePosition`.
- Автоматическое создание неявного промежуточного класса сущностей `EmployeePosition`. Его имя составлено путём соединения двух имен связанных сущностей в алфавитном порядке. Если уже существует сущность с таким же именем, она будет использована как промежуточная сущность. Структура этого промежуточного класса сущностей аналогична классу `entities/employee-position.ts` и имеет следующие свойства:
- Первичный ключ, созданный согласно глобальной конфигурации первичного ключа.
- Навигационное свойство `position` типа много ко одному, связанное с сущностью `Position`.
- Внешний ключ `positionId`, указывающий на `Position.id`.
- Навигационное свойство `employee` типа много ко одному, связанное с сущностью `Employee`.
- Внешний ключ `employeeId`, указывающий на `Employee.id`.Сотрудник: entities/employee.ts
```ts
// ...
import { Position } from './position'
@table()
@comment('Сотрудник')
@context(() => DB)
export class Employee extends Entity implements EntityKey {
@column()
@key()
@comment('ID сотрудника')
@identity()
id?: bigint;
// ...
@manyToMany(() => Position) # Определение навигационного свойства, связанного с противоположной стороной
positions?: Position[];
}
```
Позиция: entities/position.ts
```ts
import { Employee } from './employee'
@table()
@comment('Должность')
@context(() => DB)
export class Position extends Entity implements EntityKey {
@column()
@comment('ID должности')
@identity()
@key()
id?: bigint;
}
``` // ...
}
```
**Добавление данных в много ко многим отношение**
```ts
const сотрудник = Сотрудник.create({
имя: 'Пётр',
// ...
должности: [{
имя: 'Менеджер отдела продаж (в совмещении)',
// ...
}, {
имя: 'Заместитель директора',
// ...
}]
})
await сотрудникРепозиторий.save(сотрудник);
```
Вышеуказанный код последовательно вставляет следующие данные:
- Сотрудник, Пётр, предположительно id=1
- Таблица Должность, менеджер отдела продаж (в совмещении) [предположительно id=1] и заместитель директора [предположительно id=2]
- Таблица СотрудникДолжность [{ сотрудникаId: 1, должностиId: 1 }, { сотрудникаId: 1, должностиId: 2 }]
**Удаление связей**
```ts
const сотрудник = сотрудникРепозиторий.get(1);
сотрудник.должности = []; // Также можно установить равным null
await сотрудникРепозиторий.save(сотрудник);
```
Приведённый выше код удалит все записи с employeeId=1 из таблицы СотрудникДолжность.#### Создание файла контекста базы данных
Файл контекста базы данных `db.ts`
```ts
import { КонтекстБазыДанных, Репозиторий, репозиторий } from 'lubejs';
import { Пользователь } from './entity/пользователь';
import { Сотрудник } from './entity/сотрудник';
export class БД extends КонтекстБазыДанных {
@Repository(() => Пользователь) // Объявление атрибута репозитория, который позволяет получить доступ к репозиторию непосредственно через этот атрибут, при этом он создаётся лениво, то есть только при обращении к нему.
пользователи: Репозиторий<Пользователь>;
@Repository(() => Сотрудник)
сотрудники: Репозиторий<Сотрудник>;
}
```
### Операция над данными сущностей
Операции над данными подробно описаны в объектах репозитория и объектах запроса, поэтому здесь повторяться не будем.
- [Контекст базы данных](#контекст-базы-данных(dbcontext))
- [Объект запроса (queriable)](#объект-запроса(queriable))
- [Объект репозитория (repository)](#объект-репозитория(repository))
- [Отношения](#отношения)
### Использование конфигураций
Пожалуйста, обратитесь к [конфигурационному файлу](#конфигурационный-файл)
### Полный пример
- Объявление декоратора: [ORM](https://github.com/jovercao/lubejs-tester/blob/master/orm-decorator/index.ts)
- Объявление кода конфигурации: [ORM](https://github.com/jovercao/lubejs-tester/blob/master/orm-configure.ts)
### Использование репозитория- [Вставка](https://github.com/jovercao/lubejs-tester/blob/master/tests/repository/insert.test.ts)
- [Обновление](https://github.com/jovercao/lubejs-tester/blob/master/tests/repository/update.test.ts)
- [Удаление](https://github.com/jovercao/lubejs-tester/blob/master/tests/repository/delete.test.ts)
## Данная миграцияДля использования функции данных миграций требуется использование командной строки (CLI), которая входит в состав пакета lubejs. После установки lubejs мы получаем доступ к команде `lube`, с помощью которой можно выполнять операции по миграции данных.
### Создание конфигурационного файла
Перед использованием данных миграций необходимо создать конфигурационный файл `.lubejs.ts` или `.lubejs.js`. Без этого файла невозможно запустить инструмент миграции. Подробнее о создании конфигурационного файла см. раздел [Конфигурационный файл](#конфигурационный-файл).
### Создание файлов миграции
```shell
lube migrate add [name]
```
Эта команда создаёт файл с описанием структуры таблицы в виде `./migrates/<yyyyMMddHHmmss>_Init.ts`. Вместе с ним также создаётся файл с суффиксом `.snapshot.ts`.
Пример:
```sh
# Создание файла миграции с именем Init.
lube migrate add Init
```
Если после этого структура таблиц не была изменена, повторное выполнение следующей команды:
```ts
lube migrate add AddOrderModule
```
Результатом будет пустой файл `./migrates/<yyyyMMddHHmmss>_AddOrderModule.ts`.
### Ручное создание файлов миграции
После создания файлов миграции вы можете добавить в них код для миграции базы данных. Однако если структура таблиц отличается от последнего снимка, при следующем выполнении команды будет создан новый снимок для сравнения.
```ts
import { Migrate, SQL, DbType, MigrateBuilder } from 'lubejs';
``````typescript
export class Init implements Migrate {
async up(
builder: MigrateBuilder, // Object for building migration code
dialect: string // Database dialect used during execution
): Promise<void> {
// Here you should add the code to apply the migration
}
async down(
builder: MigrateBuilder,
dialect: string
): Promise<void> {
// Here you should add the code to rollback the migration
}
}
export default Init;
```
All migration files must be created using the `MigrateBuilder` class as shown above. This object is used similarly to the `SQL` object but provides more capabilities for working with migrations rather than other types of data operations.
It's important to note that using the method `builder.sql(...)` to create migration code can lead to issues because changes in structure created this way may not be accounted for by the system when creating snapshots. This could cause errors on subsequent attempts to create migration files.
### Updating the database to a migration version
```shell
lube migrate update [name]
```
This command updates the database to the migration version named `[name]`. If the current database version is newer than the specified one, it will be rolled back to that version.
If the parameter `[name]` is not provided, it means using the latest migration version.
### Synchronizing the database
```ts
lube migrate sync
```Эта команда отличается от `update`: она анализирует текущую структуру данных объектов и обновляет структуру базы данных до соответствия этим объектам, но не выполняет код внутри файлов миграций. Обычно используется для быстрого создания тестовой среды базы данных; не рекомендуется использовать эту команду в продакшне.
### Экспорт скриптов обновления/понижения
```ts
lube migrate script --source <source_name> --target <target_name> --output <output_file>
```
Эта команда генерирует SQL-код для файла миграции, где `<source_name>` — имя файла источника миграции, `<target_name>` — имя файла целевой миграции, а результат экспортируется в файл `<output_file>`.
Дополнительная информация доступна через `lube --help`.
## Другие вопросы
### Проблемы сериализации JSON
Обычно при использовании JavaScript мы используем объект `JSON` для сериализации (например, когда отправляем данные клиенту), однако некоторые типы, такие как `BigInt`, не поддерживаются сериализацией, что приводит к ошибкам. В Lubejs специальные скалярные типы `Scalar` сериализуются следующим образом:| Тип | Описание | Конкретная ситуация |
| -------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Binary | псевдоним типа TypeScript, фактически является `Buffer`, `ArrayBuffer` или `TypedArrayBuffer` | Чтобы не загрязнять native объекты, lubejs не меняет поведение сериализации:<br>- Если значение имеет тип Buffer, вызывается `Buffer.prototype.toString()`, что может привести к появлению мусора.<br>- Если значение имеет тип ArrayBuffer, возвращается `{}`. |
| Uuid | тип UUID | Реализует `.toJSON`, возвращает строку в формате `"00000000-0000-0000-0000-000000000000"`. |
| BigInt | встроенный тип V8 | При попытке сериализации возникнет ошибка. |
| Decimal | основан на библиотеке `decimal.js-light` | Реализует `.toJSON`, сериализуется в строку, пример: `"100"`. | Рекомендованное решение следующее: |1. Пользовательская сериализация
```ts
JSON.stringify({ bigint: 1n }, (key, value) => {
if (typeof value === 'bigint') {
return value.toString();
} else {
return value;
}
});
```
2. Реализация метода `.toJSON`
```ts
BigInt.prototype.toJSON = function() { return this.toString() }
JSON.stringify(1n); // => '"1"'
```
При десериализации также следует учитывать тип.### Различия между базами данных| Функционал | MSSQL | MySQL | PostgreSQL |
| ---------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| Поддержка функций | Да | Да | Да |
| Поддержка хранимых процедур | Да | Да | Нет поддержки объявления, можно использовать только функции |
| Вызов хранимой процедуры | EXECUTE <sp_name> | CALL <sp_name> | SELECT <fn_name>(...) |
| Возврат множественного набора функцией | Нет поддержки, возвращаются только одиночные наборы | Нет поддержки, возвращаются только одиночные наборы | Да |
| Возврат множественного набора хранимыми процедурами | Да | Да | Через выходные параметры |
| Транзакционность хранимых процедур | Явная открытие | Автоматическая открытие | Автоматическая открытие || Предварительно скомпилированные запросы | Ключевое слово PREPARE | Через хранимую процедуру: sp_prepare, sp_execute, sp_unprepare, ключевое слово PREPARE | Ключевое слово PREPARE |
| Поддержка выполнения нескольких команд | Поддерживается, **поддерживаются объявление переменных, присваивание значений и операции**, а также возврат множественного набора | Поддерживается, но требуется открытие опции multiStatements и использование разделителя (обычно ";", может быть настроен); **поддерживаются только операции присваивания глобальных переменных**. Поддерживается возврат множественного набора | Поддерживается, но требуется использование разделителя ";" между командами | **Поддержка объявления и присваивания значений переменных недоступна**, поддерживается возврат множества строк с использованием блока BEGIN...END | Поддерживается использование блока BEGIN...ENDEND, который **может использоваться только внутри хранимых процедур и не связан с транзакциями**, может вернуть множество строк; эту функциональность можно достигнуть путём создания временной хранимой процедуры | В блоке DO можно выполнить операцию в любом месте кода, но она будет автоматически рассматриваться как атомарная транзакция, и не поддерживает передачу параметров и возврат множества строк; путь через переменные заблокирован, поэтому **можно использовать временные таблицы уровня транзакций для реализации этой функции**
| Объявление локальных переменных | Может выполняться в любом месте кода, локальные переменные | Доступно только внутри хранимых процедур/функций | Доступно только внутри блока DO, хранимых процедур и функций |
| Глобальные переменные | Только системные глобальные переменные, доступны в любом месте кода | Поддерживаются глобальные переменные (уровня сессии — соединение, глобального — всей базы данных), доступны в любом месте кода | Только системные глобальные переменные, создание пользовательских глобальных переменных недоступно |
| Передача параметров prepare | Через имя переменной (EXEC SP_EXECUTE), поддерживаются выходные параметры | ?Заполнители, выходные параметры не поддерживаются, могут быть достигнуты через глобальные переменные или `SELECT` | `$n` заполнители, выходные параметры не поддерживаются, могут быть достигнуты только через `SELECT` |
| Передача параметров функций/хранимых процедур | Выходные параметры могут быть переданы через ссылку, выводятся в переменные | Выходные параметры могут быть переданы через ссылку, выводятся в переменные<br>Глобальные переменные могут быть возвращены (уровня сессии, глобального) | Поддерживается только передача значений, через `SELECT` возвращаются |
| Смена базы данных | `USE <db_name>` | `USE <db_name>` | Недоступно, но можно использовать схемы вместо этого |
| Открытие транзакции | `BEGIN TRANSACTION...COMMIT...ROLLBACK` | `START TRANSACTION...COMMIT...ROLLBACK` | `BEGIN...END;`<br>`START BEGIN...COMMIT...ROLLBACK` | **реализация вывода параметров в PostgreSQL:** |```plsql
begin;
do $$ declare a int = 1;
begin
a := 200;
create local temporary table xyz on commit drop as select a;
end$$;
select * from xyz;
end;
```
## API
[Документация API](./doc/globals.md)
## Задачи
- [ ] Поддержка драйвера MySQL
- [ ] Поддержка драйвера PostgreSQL
- [ ] Поддержка драйвера SQLite
- [x] Улучшение охвата тестов до 85%
- [ ] Оптимизация производительности
- [x] Преобразование множественных запросов между главной базой данных и репликами в одиночный запрос
- [ ] Оптимизация производительности операций вставки, удаления, выборки и изменения, сокращение компиляции SQL
## Обновленные логи
### 3.0.0-preview06
- Исправление ошибок
### 3.0.0-preview05
- Преобразование множественных запросов между главной базой данных и репликами в одиночный запрос
- Добавление сериализации UUID
- Отключение кэширования запросов
- Исправление ошибок
### 3.0.0-preview04
- Исправление некоторых ошибок
- Изменение `Repository.prototype.get` так, чтобы выбрасывалось исключение при отсутствии ключа
- Создание первоначальной версии документации
- Увеличение охвата тестов
### 3.0.0-preview01
Первоначальная версия для просмотра
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Комментарии ( 0 )