Перейти к основному содержанию

Начало работы

Неофициальный Бета-перевод

Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →

TypeORM — это ORM, работающая на платформах Node.js, Browser, Cordova, Ionic, React Native, NativeScript, Expo и Electron, и поддерживающая TypeScript и JavaScript (ES2021).

Её цель — постоянно поддерживать новейшие возможности JavaScript и предоставлять дополнительные функции, помогающие разрабатывать любые приложения, использующие базы данных: от небольших приложений с несколькими таблицами до крупных корпоративных систем с множеством баз данных.

TypeORM поддерживает больше баз данных, чем любая другая JS/TS ORM: Google Spanner, Microsoft SqlServer, MongoDB, MySQL/MariaDB, Oracle, Postgres, SAP HANA и SQLite, а также производные базы данных и различные драйверы.

В отличие от всех существующих JavaScript ORM, TypeORM поддерживает как шаблон Active Record, так и Data Mapper, что позволяет создавать высококачественные, слабосвязанные, масштабируемые и поддерживаемые приложения максимально эффективно.

На TypeORM сильно повлияли другие ORM, такие как Hibernate, Doctrine и Entity Framework.

Возможности

  • Поддержка как DataMapper, так и ActiveRecord (на выбор).

  • Сущности и колонки.

  • Специфичные для БД типы колонок.

  • Менеджер сущностей.

  • Репозитории и пользовательские репозитории.

  • Чистая объектно-реляционная модель.

  • Ассоциации (связи).

  • Жадные (eager) и ленивые (lazy) связи.

  • Однонаправленные, двунаправленные и самореферентные связи.

  • Поддержка нескольких паттернов наследования.

  • Каскадные операции.

  • Индексы.

  • Транзакции.

  • Миграции с автоматической генерацией.

  • Пул соединений.

  • Репликация.

  • Работа с несколькими экземплярами БД.

  • Поддержка нескольких типов баз данных.

  • Межбазовые и межсхемные запросы.

  • Элегантный, гибкий и мощный QueryBuilder.

  • Левые (left) и внутренние (inner) соединения.

  • Корректная пагинация для запросов с соединениями.

  • Кэширование запросов.

  • Потоковая обработка сырых результатов.

  • Логирование.

  • Слушатели и подписчики (hooks).

  • Поддержка паттерна Closure Table.

  • Объявление схемы в моделях или отдельных конфигурационных файлах.

  • Поддержка MySQL / MariaDB / Postgres / CockroachDB / SQLite / Microsoft SQL Server / Oracle / SAP Hana / sql.js.

  • Поддержка NoSQL-базы данных MongoDB.

  • Работа на платформах Node.js / Browser / Ionic / Cordova / React Native / NativeScript / Expo / Electron.

  • Поддержка TypeScript и JavaScript.

  • Поддержка ESM и CommonJS.

  • Генерация производительного, гибкого, чистого и поддерживаемого кода.

  • Следование лучшим практикам.

  • CLI-интерфейс.

И многое другое...

С TypeORM ваши модели выглядят так:

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number

@Column()
firstName: string

@Column()
lastName: string

@Column()
age: number
}

А бизнес-логика выглядит так:

const userRepository = AppDataSource.getRepository(User)

const user = new User()
user.firstName = "Timber"
user.lastName = "Saw"
user.age = 25
await userRepository.save(user)

const allUsers = await userRepository.find()
const firstUser = await userRepository.findOneBy({
id: 1,
}) // find by id
const timber = await userRepository.findOneBy({
firstName: "Timber",
lastName: "Saw",
}) // find by firstName and lastName

await userRepository.remove(timber)

Если вы предпочитаете подход ActiveRecord, вы также можете использовать его:

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.findOneBy({
id: 1,
})
const timber = await User.findOneBy({
firstName: "Timber",
lastName: "Saw",
})

await timber.remove()

Установка

  1. Установите npm-пакет:

    npm install typeorm

  2. Установите shim для reflect-metadata:

    npm install reflect-metadata

    и импортируйте его в глобальной области вашего приложения (например, в app.ts):

    import "reflect-metadata"

  3. Возможно, потребуется установить типы для Node.js:

    npm install @types/node --save-dev

  4. Установите драйвер базы данных: см. документацию для каждого конкретного драйвера: mongodb, mssql, mysql/mariadb, oracle, postgres, sap, spanner, sqlite.

Настройка TypeScript

Также убедитесь, что используете TypeScript версии 4.5 или выше, и активировали следующие настройки в tsconfig.json:

"emitDecoratorMetadata": true,
"experimentalDecorators": true,

Быстрый старт

Самый быстрый способ начать работу с TypeORM — использовать CLI для генерации стартового проекта. Это работает только для Node.js приложений. Для других платформ перейдите к пошаговому руководству.

Чтобы создать новый проект через CLI, выполните команду:

npx typeorm init --name MyProject --database postgres

Где name — имя вашего проекта, а database — используемая СУБД. Допустимые значения для database: mysql, mariadb, postgres, cockroachdb, sqlite, mssql, sap, spanner, oracle, mongodb, cordova, react-native, expo, nativescript.

Эта команда сгенерирует новый проект в директории MyProject со следующими файлами:

MyProject
├── src // place of your TypeScript code
│ ├── entities // place where your entities (database models) are stored
│ │ └── User.ts // sample entity
│ ├── migrations // place where your migrations are stored
│ ├── data-source.ts // data source and all connection configuration
│ └── index.ts // start point of your application
├── .gitignore // standard gitignore file
├── package.json // node module dependencies
├── README.md // simple readme file
└── tsconfig.json // TypeScript compiler options

Вы также можете выполнить typeorm init в существующем node-проекте, но будьте осторожны — это может перезаписать существующие файлы.

Следующий шаг — установка зависимостей проекта:

cd MyProject
npm install

После установки зависимостей отредактируйте файл data-source.ts, указав параметры подключения к вашей БД:

export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "test",
password: "test",
database: "test",
synchronize: true,
logging: true,
entities: [Post, Category],
subscribers: [],
migrations: [],
})

Как правило, потребуется настроить только host, username, password, database и, возможно, port.

После завершения настройки и установки модулей запустите приложение:

npm start

Готово! Приложение успешно запустится и добавит нового пользователя в базу данных. Вы можете продолжить работу с этим проектом: добавить необходимые модули и создавать новые сущности.

Для генерации ESM-проекта выполните: npx typeorm init --name MyProject --database postgres --module esm

Для генерации продвинутого проекта с Express выполните: npx typeorm init --name MyProject --database mysql --express

Для генерации docker-compose файла выполните: npx typeorm init --name MyProject --database postgres --docker

Пошаговое руководство

Чего вы ожидаете от ORM? Прежде всего, вы ожидаете, что она будет создавать таблицы в базе данных и находить/добавлять/обновлять/удалять ваши данные без необходимости писать множество сложных в поддержке SQL-запросов. Это руководство покажет, как настроить TypeORM с нуля и заставить его делать то, что вы ожидаете от ORM.

Создание модели

Работа с базой данных начинается с создания таблиц. Как указать TypeORM создать таблицу в базе данных? Ответ — через модели. Ваши модели в приложении — это ваши таблицы в базе данных.

Например, у вас есть модель Photo:

export class Photo {
id: number
name: string
description: string
filename: string
views: number
isPublished: boolean
}

И вы хотите хранить фотографии в вашей базе данных. Чтобы хранить что-либо в базе данных, сначала вам нужна таблица, а таблицы создаются из ваших моделей. Но не из всех моделей, а только из тех, которые вы определите как сущности.

Создание сущности

Сущность — это ваша модель, помеченная декоратором @Entity. Для таких моделей будет создана таблица в базе данных. В TypeORM вы повсеместно работаете с сущностями. Вы можете загружать/добавлять/обновлять/удалять и выполнять другие операции с ними.

Давайте сделаем нашу модель Photo сущностью:

import { Entity } from "typeorm"

@Entity()
export class Photo {
id: number
name: string
description: string
filename: string
views: number
isPublished: boolean
}

Теперь для сущности Photo будет создана таблица в базе данных, и мы сможем работать с ней в любом месте нашего приложения. Мы создали таблицу, но разве может существовать таблица без столбцов? Давайте создадим несколько столбцов в нашей таблице.

Добавление столбцов таблицы

Чтобы добавить столбцы в базу данных, вам нужно пометить свойства сущности, которые вы хотите сделать столбцами, декоратором @Column.

import { Entity, Column } from "typeorm"

@Entity()
export class Photo {
@Column()
id: number

@Column()
name: string

@Column()
description: string

@Column()
filename: string

@Column()
views: number

@Column()
isPublished: boolean
}

Теперь столбцы id, name, description, filename, views и isPublished будут добавлены в таблицу photo. Типы столбцов в базе данных выводятся из типов свойств, которые вы использовали, например: number будет преобразован в integer, string — в varchar, boolean — в bool и т.д. Но вы можете использовать любой тип столбца, поддерживаемый вашей базой данных, явно указав его в декораторе @Column.

Мы создали таблицу с столбцами, но осталось одно: каждая таблица в базе данных должна иметь столбец с первичным ключом.

Создание первичного столбца

Каждая сущность обязана иметь хотя бы один столбец с первичным ключом. Это требование, и его нельзя обойти. Чтобы сделать столбец первичным ключом, используйте декоратор @PrimaryColumn.

import { Entity, Column, PrimaryColumn } from "typeorm"

@Entity()
export class Photo {
@PrimaryColumn()
id: number

@Column()
name: string

@Column()
description: string

@Column()
filename: string

@Column()
views: number

@Column()
isPublished: boolean
}

Создание автоматически генерируемого столбца

Допустим, вы хотите, чтобы столбец id генерировался автоматически (это называется автоинкрементным/последовательным/серийным столбцом). Для этого замените декоратор @PrimaryColumn на @PrimaryGeneratedColumn:

import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"

@Entity()
export class Photo {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@Column()
description: string

@Column()
filename: string

@Column()
views: number

@Column()
isPublished: boolean
}

Типы данных столбцов

Теперь давайте исправим типы данных. По умолчанию строка преобразуется в тип, похожий на varchar(255) (в зависимости от типа базы данных). Число преобразуется в целочисленный тип (также в зависимости от типа базы данных). Мы не хотим, чтобы все наши столбцы были ограничены varchar или integer. Установим правильные типы данных:

import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"

@Entity()
export class Photo {
@PrimaryGeneratedColumn()
id: number

@Column({
length: 100,
})
name: string

@Column("text")
description: string

@Column()
filename: string

@Column("double")
views: number

@Column()
isPublished: boolean
}

Типы столбцов зависят от базы данных. Вы можете установить любой тип столбца, который поддерживается вашей базой данных. Дополнительную информацию о поддерживаемых типах столбцов можно найти здесь.

Создание нового DataSource

Теперь, когда наша сущность создана, давайте создадим файл index.ts и настроим там наш DataSource:

import "reflect-metadata"
import { DataSource } from "typeorm"
import { Photo } from "./entity/Photo"

const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "root",
password: "admin",
database: "test",
entities: [Photo],
synchronize: true,
logging: false,
})

// to initialize the initial connection with the database, register all entities
// and "synchronize" database schema, call "initialize()" method of a newly created database
// once in your application bootstrap
try {
await AppDataSource.initialize()
} catch (error) {
console.log(error)
}

В этом примере мы используем Postgres, но вы можете выбрать любую другую поддерживаемую базу данных. Для смены базы измените параметр type в настройках на нужный тип: mysql, mariadb, postgres, cockroachdb, sqlite, mssql, oracle, sap, spanner, cordova, nativescript, react-native, expo или mongodb. Также укажите свои параметры хоста, порта, пользователя, пароля и базы данных.

Мы добавили сущность Photo в список сущностей для этого источника данных. Все сущности, используемые в подключении, должны быть перечислены здесь.

Параметр synchronize гарантирует синхронизацию ваших сущностей с базой данных при каждом запуске приложения.

Запуск приложения

При запуске index.ts установится соединение с базой данных и создастся таблица для фотографий.

+-------------+--------------+----------------------------+
| photo |
+-------------+--------------+----------------------------+
| id | int | PRIMARY KEY AUTO_INCREMENT |
| name | varchar(100) | |
| description | text | |
| filename | varchar(255) | |
| views | int | |
| isPublished | boolean | |
+-------------+--------------+----------------------------+

Создание и вставка фотографии в базу данных

Создадим новую фотографию для сохранения в базе:

import { Photo } from "./entity/Photo"
import { AppDataSource } from "./index"

const photo = new Photo()
photo.name = "Me and Bears"
photo.description = "I am near polar bears"
photo.filename = "photo-with-bears.jpg"
photo.views = 1
photo.isPublished = true

await AppDataSource.manager.save(photo)
console.log("Photo has been saved. Photo id is", photo.id)

После сохранения сущности ей присваивается новый сгенерированный id. Метод save возвращает тот же объект, который вы передали (не копию), модифицируя его поле "id".

Использование EntityManager

Мы создали и сохранили новую фотографию через EntityManager. С помощью менеджера сущностей можно управлять любой сущностью. Например, загрузим сохранённую сущность:

import { Photo } from "./entity/Photo"
import { AppDataSource } from "./index"

const savedPhotos = await AppDataSource.manager.find(Photo)
console.log("All photos from the db: ", savedPhotos)

savedPhotos будет массивом объектов Photo с данными из базы.

Подробнее об EntityManager.

Использование репозиториев

Реорганизуем код, используя Repository вместо EntityManager. Каждая сущность имеет собственный репозиторий для операций с ней. При активной работе с сущностями репозитории удобнее:

import { Photo } from "./entity/Photo"
import { AppDataSource } from "./index"

const photo = new Photo()
photo.name = "Me and Bears"
photo.description = "I am near polar bears"
photo.filename = "photo-with-bears.jpg"
photo.views = 1
photo.isPublished = true

const photoRepository = AppDataSource.getRepository(Photo)

await photoRepository.save(photo)
console.log("Photo has been saved")

const savedPhotos = await photoRepository.find()
console.log("All photos from the db: ", savedPhotos)

Подробнее о репозиториях здесь.

Загрузка данных из базы

Попробуем дополнительные операции загрузки через репозиторий:

import { Photo } from "./entity/Photo"
import { AppDataSource } from "./index"

const photoRepository = AppDataSource.getRepository(Photo)
const allPhotos = await photoRepository.find()
console.log("All photos from the db: ", allPhotos)

const firstPhoto = await photoRepository.findOneBy({
id: 1,
})
console.log("First photo from the db: ", firstPhoto)

const meAndBearsPhoto = await photoRepository.findOneBy({
name: "Me and Bears",
})
console.log("Me and Bears photo from the db: ", meAndBearsPhoto)

const allViewedPhotos = await photoRepository.findBy({ views: 1 })
console.log("All viewed photos: ", allViewedPhotos)

const allPublishedPhotos = await photoRepository.findBy({ isPublished: true })
console.log("All published photos: ", allPublishedPhotos)

const [photos, photosCount] = await photoRepository.findAndCount()
console.log("All photos: ", photos)
console.log("Photos count: ", photosCount)

Обновление данных в базе

Загрузим отдельную фотографию из базы, обновим её и сохраним:

import { Photo } from "./entity/Photo"
import { AppDataSource } from "./index"

const photoRepository = AppDataSource.getRepository(Photo)
const photoToUpdate = await photoRepository.findOneBy({
id: 1,
})
photoToUpdate.name = "Me, my friends and polar bears"
await photoRepository.save(photoToUpdate)

Фотография с id = 1 теперь обновлена в базе.

Удаление из базы данных

Удалим нашу фотографию из базы:

import { Photo } from "./entity/Photo"
import { AppDataSource } from "./index"

const photoRepository = AppDataSource.getRepository(Photo)
const photoToRemove = await photoRepository.findOneBy({
id: 1,
})
await photoRepository.remove(photoToRemove)

Фотография с id = 1 теперь удалена из базы.

Создание связи один-к-одному

Создадим связь один-к-одному с другим классом. Добавим новый класс в PhotoMetadata.ts для хранения метаданных фотографии:

import {
Entity,
Column,
PrimaryGeneratedColumn,
OneToOne,
JoinColumn,
} from "typeorm"
import { Photo } from "./Photo"

@Entity()
export class PhotoMetadata {
@PrimaryGeneratedColumn()
id: number

@Column("int")
height: number

@Column("int")
width: number

@Column()
orientation: string

@Column()
compressed: boolean

@Column()
comment: string

@OneToOne(() => Photo)
@JoinColumn()
photo: Photo
}

Здесь используется декоратор @OneToOne для создания связи между сущностями. Декоратор @JoinColumn указывает владельца связи. Связи бывают однонаправленными или двунаправленными, но владельцем может быть только одна сторона. @JoinColumn обязателен на стороне владельца.

После запуска приложения вы увидите новую таблицу со столбцом внешнего ключа для связи с фотографией:

+-------------+--------------+----------------------------+
| photo_metadata |
+-------------+--------------+----------------------------+
| id | int | PRIMARY KEY AUTO_INCREMENT |
| height | int | |
| width | int | |
| comment | varchar(255) | |
| compressed | boolean | |
| orientation | varchar(255) | |
| photoId | int | FOREIGN KEY |
+-------------+--------------+----------------------------+

Сохранение связи один-к-одному

Теперь сохраним фотографию и её метаданные, связав их между собой.

import { Photo } from "./entity/Photo"
import { PhotoMetadata } from "./entity/PhotoMetadata"

// Create a photo
const photo = new Photo()
photo.name = "Me and Bears"
photo.description = "I am near polar bears"
photo.filename = "photo-with-bears.jpg"
photo.views = 1
photo.isPublished = true

// Create a photo metadata
const metadata = new PhotoMetadata()
metadata.height = 640
metadata.width = 480
metadata.compressed = true
metadata.comment = "cybershoot"
metadata.orientation = "portrait"
metadata.photo = photo // this way we connect them

// Get entity repositories
const photoRepository = AppDataSource.getRepository(Photo)
const metadataRepository = AppDataSource.getRepository(PhotoMetadata)

// First we should save a photo
await photoRepository.save(photo)

// The Photo is saved. Now we need to save a photo metadata
await metadataRepository.save(metadata)

// Done
console.log(
"Metadata is saved, and the relation between metadata and photo is created in the database too",
)

Обратная сторона связи

Связи могут быть однонаправленными или двунаправленными. В текущей реализации связь между PhotoMetadata и Photo является однонаправленной. Владельцем связи выступает PhotoMetadata, а Photo ничего не знает о PhotoMetadata. Это усложняет доступ к PhotoMetadata со стороны Photo. Чтобы решить эту проблему, следует добавить обратную связь и сделать отношения между PhotoMetadata и Photo двунаправленными. Изменяем наши сущности:

import {
Entity,
Column,
PrimaryGeneratedColumn,
OneToOne,
JoinColumn,
} from "typeorm"
import { Photo } from "./Photo"

@Entity()
export class PhotoMetadata {
/* ... other columns */

@OneToOne(() => Photo, (photo) => photo.metadata)
@JoinColumn()
photo: Photo
}
import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from "typeorm"
import { PhotoMetadata } from "./PhotoMetadata"

@Entity()
export class Photo {
/* ... other columns */

@OneToOne(() => PhotoMetadata, (photoMetadata) => photoMetadata.photo)
metadata: PhotoMetadata
}

photo => photo.metadata — это функция, возвращающая имя обратной стороны отношения. Здесь показано, что свойство metadata класса Photo хранит связь с PhotoMetadata внутри класса Photo. Вместо передачи функции, возвращающей свойство photo, вы можете передать строку в декоратор @OneToOne, например "metadata". Но мы использовали функциональный подход для упрощения рефакторинга.

Важно: декоратор @JoinColumn следует использовать только с одной стороны связи. Сторона, на которой размещён этот декоратор, станет владеющей стороной отношения. Владеющая сторона содержит столбец с внешним ключом в базе данных.

Связи в проектах ESM

При использовании ESM в TypeScript-проектах применяйте тип-обёртку Relation для свойств связей, чтобы избежать проблем с циклическими зависимостями. Изменяем наши сущности:

import {
Entity,
Column,
PrimaryGeneratedColumn,
OneToOne,
JoinColumn,
Relation,
} from "typeorm"
import { Photo } from "./Photo"

@Entity()
export class PhotoMetadata {
/* ... other columns */

@OneToOne(() => Photo, (photo) => photo.metadata)
@JoinColumn()
photo: Relation<Photo>
}
import {
Entity,
Column,
PrimaryGeneratedColumn,
OneToOne,
Relation,
} from "typeorm"
import { PhotoMetadata } from "./PhotoMetadata"

@Entity()
export class Photo {
/* ... other columns */

@OneToOne(() => PhotoMetadata, (photoMetadata) => photoMetadata.photo)
metadata: Relation<PhotoMetadata>
}

Загрузка объектов со связанными сущностями

Теперь загрузим фото и связанные метаданные одним запросом. Есть два подхода: через методы find* или функционал QueryBuilder. Сначала используем метод find*. Методы find* позволяют указать объект с интерфейсом FindOneOptions / FindManyOptions.

import { Photo } from "./entity/Photo"
import { PhotoMetadata } from "./entity/PhotoMetadata"
import { AppDataSource } from "./index"

const photoRepository = AppDataSource.getRepository(Photo)
const photos = await photoRepository.find({
relations: {
metadata: true,
},
})

Теперь photos будет содержать массив фотографий из БД, каждая — со своими метаданными. Подробнее о параметрах поиска в документации.

Использование параметров поиска просто и эффективно, но для сложных запросов лучше подойдёт QueryBuilder. QueryBuilder обеспечивает элегантную работу со сложными запросами:

import { Photo } from "./entity/Photo"
import { PhotoMetadata } from "./entity/PhotoMetadata"
import { AppDataSource } from "./index"

const photos = await AppDataSource.getRepository(Photo)
.createQueryBuilder("photo")
.innerJoinAndSelect("photo.metadata", "metadata")
.getMany()

QueryBuilder позволяет создавать и выполнять SQL-запросы практически любой сложности. Работая с QueryBuilder, действуйте как при создании SQL-запроса. В этом примере "photo" и "metadata" — алиасы для выбранных фотографий. Алиасы дают доступ к колонкам и свойствам выбранных данных.

Автоматическое сохранение связанных объектов через каскады

Настроим каскадное сохранение для случаев, когда связанный объект должен сохраняться автоматически при сохранении основного объекта. Модифицируем декоратор @OneToOne для фото:

export class Photo {
// ... other columns

@OneToOne(() => PhotoMetadata, (metadata) => metadata.photo, {
cascade: true,
})
metadata: PhotoMetadata
}

Параметр cascade избавляет от необходимости отдельно сохранять фото и метаданные. Теперь достаточно сохранить объект фото — метаданные сохранятся автоматически благодаря каскаду.

import { AppDataSource } from "./index"

// create photo object
const photo = new Photo()
photo.name = "Me and Bears"
photo.description = "I am near polar bears"
photo.filename = "photo-with-bears.jpg"
photo.isPublished = true

// create photo metadata object
const metadata = new PhotoMetadata()
metadata.height = 640
metadata.width = 480
metadata.compressed = true
metadata.comment = "cybershoot"
metadata.orientation = "portrait"

photo.metadata = metadata // this way we connect them

// get repository
const photoRepository = AppDataSource.getRepository(Photo)

// saving a photo also save the metadata
await photoRepository.save(photo)

console.log("Photo is saved, photo metadata is saved too.")

Обратите внимание: мы устанавливаем свойство metadata фото, а не свойство photo метаданных, как раньше. Свойство cascade работает только при связывании фото с метаданными со стороны фото. При установке связи со стороны метаданных автосохранение работать не будет.

Создание связи "многие-к-одному" / "один-ко-многим"

Создадим связь "многие-к-одному"/"один-ко-многим". Предположим, у фото один автор, а у автора много фото. Сначала создадим класс Author:

import {
Entity,
Column,
PrimaryGeneratedColumn,
OneToMany,
JoinColumn,
} from "typeorm"
import { Photo } from "./Photo"

@Entity()
export class Author {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@OneToMany(() => Photo, (photo) => photo.author) // note: we will create author property in the Photo class below
photos: Photo[]
}

Author содержит обратную сторону связи. OneToMany всегда является обратной стороной и не может существовать без ManyToOne на противоположной стороне.

Теперь добавим владеющую сторону связи в сущность Photo:

import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from "typeorm"
import { PhotoMetadata } from "./PhotoMetadata"
import { Author } from "./Author"

@Entity()
export class Photo {
/* ... other columns */

@ManyToOne(() => Author, (author) => author.photos)
author: Author
}

В связях "многие-к-одному"/"один-ко-многим" владеющая сторона всегда имеет тип "многие-к-одному". Это значит, что класс с @ManyToOne будет хранить id связанного объекта.

После запуска приложения ORM создаст таблицу author:

+-------------+--------------+----------------------------+
| author |
+-------------+--------------+----------------------------+
| id | int | PRIMARY KEY AUTO_INCREMENT |
| name | varchar(255) | |
+-------------+--------------+----------------------------+

Он также изменит таблицу photo, добавив новый столбец author и создав для него внешний ключ:

+-------------+--------------+----------------------------+
| photo |
+-------------+--------------+----------------------------+
| id | int | PRIMARY KEY AUTO_INCREMENT |
| name | varchar(255) | |
| description | varchar(255) | |
| filename | varchar(255) | |
| isPublished | boolean | |
| authorId | int | FOREIGN KEY |
+-------------+--------------+----------------------------+

Создание связи «многие ко многим»

Давайте создадим связь «многие ко многим». Предположим, фотография может находиться в нескольких альбомах, а каждый альбом может содержать множество фотографий. Создадим класс Album:

import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToMany,
JoinTable,
} from "typeorm"

@Entity()
export class Album {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@ManyToMany(() => Photo, (photo) => photo.albums)
@JoinTable()
photos: Photo[]
}

Декоратор @JoinTable обязателен для указания, что эта сторона является владельцем отношения.

Теперь добавим обратную сторону связи в класс Photo:

export class Photo {
// ... other columns

@ManyToMany(() => Album, (album) => album.photos)
albums: Album[]
}

После запуска приложения ORM создаст связующую таблицу (junction table) с именем album_photos_photo_albums:

+-------------+--------------+----------------------------+
| album_photos_photo_albums |
+-------------+--------------+----------------------------+
| album_id | int | PRIMARY KEY FOREIGN KEY |
| photo_id | int | PRIMARY KEY FOREIGN KEY |
+-------------+--------------+----------------------------+

Не забудьте зарегистрировать класс Album в подключении ORM:

const options: DataSourceOptions = {
// ... other options
entities: [Photo, PhotoMetadata, Author, Album],
}

Теперь добавим альбомы и фотографии в базу данных:

import { AppDataSource } from "./index"

// create a few albums
const album1 = new Album()
album1.name = "Bears"
await AppDataSource.manager.save(album1)

const album2 = new Album()
album2.name = "Me"
await AppDataSource.manager.save(album2)

// create a few photos
const photo = new Photo()
photo.name = "Me and Bears"
photo.description = "I am near polar bears"
photo.filename = "photo-with-bears.jpg"
photo.views = 1
photo.isPublished = true
photo.albums = [album1, album2]
await AppDataSource.manager.save(photo)

// now our photo is saved and albums are attached to it
// now lets load them:
const loadedPhoto = await AppDataSource.getRepository(Photo).findOne({
where: {
id: 1,
},
relations: {
albums: true,
},
})

Значение loadedPhoto будет следующим:

{
id: 1,
name: "Me and Bears",
description: "I am near polar bears",
filename: "photo-with-bears.jpg",
albums: [{
id: 1,
name: "Bears"
}, {
id: 2,
name: "Me"
}]
}

Использование QueryBuilder

Вы можете использовать QueryBuilder для создания SQL-запросов практически любой сложности. Например:

const photos = await AppDataSource.getRepository(Photo)
.createQueryBuilder("photo") // First argument is an alias. Alias is what you are selecting - photos. You must specify it.
.innerJoinAndSelect("photo.metadata", "metadata")
.leftJoinAndSelect("photo.albums", "album")
.where("photo.isPublished = true")
.andWhere("(photo.name = :photoName OR photo.name = :bearName)")
.orderBy("photo.id", "DESC")
.skip(5)
.take(10)
.setParameters({ photoName: "My", bearName: "Mishka" })
.getMany()

Этот запрос выбирает все опубликованные фотографии с именами "My" или "Mishka". Результаты будут выбраны начиная с 5-й позиции (смещение пагинации) с ограничением в 10 элементов. Сортировка выполняется по id в порядке убывания. Альбомы фотографий присоединяются через left join, а их метаданные — через inner join.

QueryBuilder интенсивно используется в приложениях. Подробнее о нём читайте здесь.

Примеры

Изучите примеры использования в директории sample.

Доступные для клонирования стартовые репозитории:

Расширения

Существует несколько расширений, упрощающих работу с TypeORM и его интеграцию с другими модулями:

Участие в разработке

Узнайте о внесении вклада здесь и о настройке среды разработки здесь.

Этот проект существует благодаря всем участникам:

Спонсоры

Разработка open-source требует значительных усилий и времени. Если вы хотите поддержать будущее TypeORM, вы можете стать спонсором, что позволит основной команде уделять больше времени улучшениям и новым функциям. Стать спонсором

Золотые спонсоры

Станьте золотым спонсором и получите премиальную техническую поддержку от основных контрибьюторов. Стать золотым спонсором