@10pearls/typeorm 中文文档教程

发布于 3年前 浏览 34 项目主页 更新于 3年前

TypeORM 是一个 ORM 可以在 NodeJS、浏览器、Cordova、PhoneGap、Ionic、React Native、NativeScript、Expo 和 Electron 平台上运行 并且可以与 TypeScript 和 JavaScript(ES5、ES6、ES7、ES8)一起使用。 它的目标是始终支持最新的 JavaScript 特性并提供额外的特性 帮助您开发任何类型的使用数据库的应用程序 - 从 从只有几张表的小型应用程序到大型企业应用程序 与多个数据库。

TypeORM 同时支持 Active Record 和 Data Mapper 模式, 与目前存在的所有其他 JavaScript ORM 不同, 这意味着您可以编写高质量、松散耦合、可扩展、 可维护的应用程序最高效的方式。

TypeORM 深受其他 ORM 的影响,例如 HibernateDoctrine实体框架

一些 TypeORM 功能:

  • supports both DataMapper and ActiveRecord (your choice)
  • entities and columns
  • database-specific column types
  • entity manager
  • repositories and custom repositories
  • clean object relational model
  • associations (relations)
  • eager and lazy relations
  • uni-directional, bi-directional and self-referenced relations
  • supports multiple inheritance patterns
  • cascades
  • indices
  • transactions
  • migrations and automatic migrations generation
  • connection pooling
  • replication
  • using multiple database connections
  • working with multiple databases types
  • cross-database and cross-schema queries
  • elegant-syntax, flexible and powerful QueryBuilder
  • left and inner joins
  • proper pagination for queries using joins
  • query caching
  • streaming raw results
  • logging
  • listeners and subscribers (hooks)
  • supports closure table pattern
  • schema declaration in models or separate configuration files
  • connection configuration in json / xml / yml / env formats
  • supports MySQL / MariaDB / Postgres / SQLite / Microsoft SQL Server / Oracle / sql.js
  • supports MongoDB NoSQL database
  • works in NodeJS / Browser / Ionic / Cordova / React Native / NativeScript / Expo / Electron platforms
  • TypeScript and JavaScript support
  • produced code is performant, flexible, clean and maintainable
  • follows all possible best practices
  • 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 user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.age = 25;
await repository.save(user);

const allUsers = await repository.find();
const firstUser = await repository.findOne(1); // find by id
const timber = await repository.findOne({ firstName: "Timber", lastName: "Saw" });

await repository.remove(timber);

或者,如果您更喜欢使用 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.findOne(1);
const timber = await User.findOne({ firstName: "Timber", lastName: "Saw" });

await timber.remove();

Installation

  1. 安装 npm 包:

    npm install typeorm --save

  2. 您需要安装 reflect-metadata shim:

    npm install reflect-metadata - - 保存

    并将其导入应用全局位置的某处(例如app.ts):

    import "reflect-metadata";

  3. 您可能需要安装节点类型:

    npm install @types/node --save

  4. 安装数据库驱动程序:

    • MySQLMariaDB

      npm install mysql --save (你也可以安装 mysql2)

    • for PostgreSQL

      npm install pg --save

    • for SQLite

      npm install sqlite3 --save

    • Microsoft SQL Server

      npm install mssql --save

    • for sql.js

      npm install sql.js --save

    • for Oracle

      npm install oracledb --save

      只安装其中的一个,具体取决于您使用的数据库。

      要使 Oracle 驱动程序正常工作,您需要按照安装说明进行操作 他们的站点。

    • MongoDB(实验性)

      npm install mongodb --save

    • NativeScriptreact-nativeCordova

      检查支持平台的文档

TypeScript configuration

另外,请确保您使用的是 2.3 或更高版本的 TypeScript 编译器, 并且您在 tsconfig.json 中启用了以下设置:

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

您可能还需要在编译器选项的 lib 部分启用 es6,或者安装@types 中的 es6-shim

Quick Start

开始使用 TypeORM 的最快方法是使用其 CLI 命令生成一个入门项目。 只有在 NodeJS 应用程序中使用 TypeORM 时,快速入门才有效。 如果您使用的是其他平台,请继续阅读分步指南

首先,全局安装TypeORM:

npm install typeorm -g

然后进入你要创建新项目的目录,运行命令:

typeorm init --name MyProject --database mysql

其中name是你的项目名称,database是您将使用的数据库。 数据库可以是以下值之一:mysqlmariadbpostgressqlitemssql< /code>, oracle, mongodb, cordovareact-nativeexponativescript

此命令将在 MyProject 目录中生成一个包含以下文件的新项目:

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

您也可以在现有节点项目上运行 typeorm init,但要小心 - 它可能会覆盖一些您已有的文件。

下一步是安装新的项目依赖项:

cd MyProject
npm install

在安装过程中,编辑 ormconfig.json 文件并在其中放置您自己的数据库连接配置选项:

{
   "type": "mysql",
   "host": "localhost",
   "port": 3306,
   "username": "test",
   "password": "test",
   "database": "test",
   "synchronize": true,
   "logging": false,
   "entities": [
      "src/entity/**/*.ts"
   ],
   "migrations": [
      "src/migration/**/*.ts"
   ],
   "subscribers": [
      "src/subscriber/**/*.ts"
   ]
}

特别是,大多数时候您只会需要配置 hostusernamepassworddatabaseport 选项。

完成配置并安装所有节点模块后,您就可以运行您的应用程序了:

npm start

就是这样,您的应用程序应该成功运行并将一个新用户插入到数据库中。 您可以继续使用此项目并集成您需要的其他模块并开始 创建更多实体。

您可以通过运行安装 express 来生成更高级的项目 typeorm init --name MyProject --database mysql --express 命令。

Step-by-Step Guide

您对 ORM 有什么期望? 首先,您期望它会为您创建数据库表 轻松查找/插入/更新/删除您的数据 不得不编写大量难以维护的 SQL 查询。 本指南将向您展示如何从头开始设置 TypeORM 并使其执行您对 ORM 的期望。

Create a model

使用数据库从创建表开始。 你如何告诉 TypeORM 创建数据库表? 答案是——通过模型。 您应用中的模型就是您的数据库表。

例如,您有一个 Photo 模型:

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

您希望将照片存储在数据库中。 要在数据库中存储东西,首先你需要一个数据库表, 和数据库表是根据您的模型创建的。 不是所有模型,而是您定义为实体的模型。

Create an entity

Entity 是由 @Entity 装饰器装饰的模型。 将为此类模型创建一个数据库表。 您可以使用 TypeORM 在任何地方处理实体。 您可以使用它们加载/插入/更新/删除和执行其他操作。

让我们将我们的 Photo 模型作为一个实体:

import {Entity} from "typeorm";

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

现在,将为 Photo 实体创建一个数据库表,我们将能够在我们的应用程序的任何地方使用它。 我们创建了一个数据库表,但是没有列的表可以存在吗? 让我们在数据库表中创建一些列。

Adding table columns

要添加数据库列,您只需将要制作的实体的属性装饰成列 使用 @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;
}

现在 idnamedescriptionfilenameviewsisPublished 列将添加到 photo 表中。 数据库中的列类型是从您使用的属性类型推断出来的,例如 number会被转成integerstring会转成varcharboolean会转成bool 等 但是您可以通过在 @Column 装饰器中隐式指定一个列类型来使用您的数据库支持的任何列类型。

我们生成了一个包含列的数据库表,但还剩下一件事。 每个数据库表都必须有一个带有主键的列。

Creating a primary 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;
}

Creating an auto generated column

现在,假设您希望自动生成您的 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;
}

Column data types

接下来,让我们修复我们的数据类型。 默认情况下,字符串被映射到类似 varchar(255) 的类型(取决于数据库类型)。 数字被映射到类似整数的类型(取决于数据库类型)。 我们不希望我们所有的列都是有限的 varchars 或整数。 让我们设置正确的数据类型:

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;
}

列类型是特定于数据库的。 您可以设置数据库支持的任何列类型。 有关支持的列类型的更多信息,请参见此处

Creating a connection to the database

现在,当我们的实体被创建时,让我们创建一个 index.ts(或 app.ts 无论你怎么称呼它)文件并在那里设置我们的连接:

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

createConnection({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "root",
    password: "admin",
    database: "test",
    entities: [
        Photo
    ],
    synchronize: true,
    logging: false
}).then(connection => {
    // here you can start to work with your entities
}).catch(error => console.log(error));

我们正在使用 MySQL在此示例中,但您可以使用任何其他受支持的数据库。 要使用另一个数据库,只需将选项中的 type 更改为您正在使用的数据库类型: mysql、mariadb、postgres、sqlite、mssql、oracle、cordova、nativescript、react-native、expo 或 mongodb。 还要确保使用您自己的主机、端口、用户名、密码和数据库设置。

我们将 Photo 实体添加到此连接的实体列表中。 您在连接中使用的每个实体都必须列在那里。

设置 synchronize 可确保您的实体在每次运行应用程序时都与数据库同步。

Loading all entities from the directory

稍后,当我们创建更多实体时,我们需要将它们添加到配置中的实体中。 这不是很方便,所以我们可以设置整个目录,所有实体都将从那里连接并在我们的连接中使用:

import {createConnection} from "typeorm";

createConnection({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "root",
    password: "admin",
    database: "test",
    entities: [
        __dirname + "/entity/*.js"
    ],
    synchronize: true,
}).then(connection => {
    // here you can start to work with your entities
}).catch(error => console.log(error));

但要小心使用这种方法。 如果您使用的是 ts-node,那么您需要指定 .ts 文件的路径。 如果您使用 outDir,那么您需要在 outDir 目录中指定 .js 文件的路径。 如果您正在使用 outDir 并且当您删除或重命名您的实体时,请确保清除 outDir 目录 并再次重新编译您的项目,因为当您删除源 .ts 文件时,它们的编译 .js 版本 不会从输出目录中删除,并且仍然由 TypeORM 加载,因为它们存在于 outDir 目录中。

Running the application

现在,如果您运行 index.ts,将初始化与数据库的连接,并为您的照片创建一个数据库表。

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

Creating and inserting a photo into the database

现在让我们创建一张新照片以将其保存在数据库中:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(connection => {

    let 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;

 &nbsp; &nbsp;return connection.manager
            .save(photo)
            .then(photo => {
                console.log("Photo has been saved. Photo id is", photo.id);
            });

}).catch(error => console.log(error));

一旦您的实体被保存,它将获得一个新生成的 ID。 save 方法返回您传递给它的同一对象的实例。 它不是对象的新副本,它修改了它的“id”并返回它。

Using async/await syntax

让我们利用最新的 ES8 (ES2017) 功能并改用 async/await 语法:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    let 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 connection.manager.save(photo);
    console.log("Photo has been saved");

}).catch(error => console.log(error));

Using Entity Manager

我们刚刚创建了一张新照片并将其保存在数据库中。 我们使用 EntityManager 来保存它。 使用实体管理器,您可以操作应用程序中的任何实体。 例如,让我们加载已保存的实体:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let savedPhotos = await connection.manager.find(Photo);
    console.log("All photos from the db: ", savedPhotos);

}).catch(error => console.log(error));

savedPhotos 将是一个包含从数据库加载的数据的 Photo 对象数组。

此处了解有关 EntityManager 的更多信息。

Using Repositories

现在让我们重构我们的代码并使用 Repository 而不是 EntityManager。 每个实体都有自己的存储库,用于处理与其实体相关的所有操作。 当您经常与实体打交道时,使用 Repositories 比 EntityManagers 更方便:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    let 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;

    let photoRepository = connection.getRepository(Photo);

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

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

}).catch(error => console.log(error));

此处了解有关 Repository 的更多信息。

Loading from the database

让我们尝试使用存储库进行更多加载操作:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let allPhotos = await photoRepository.find();
    console.log("All photos from the db: ", allPhotos);

    let firstPhoto = await photoRepository.findOne(1);
    console.log("First photo from the db: ", firstPhoto);

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

    let allViewedPhotos = await photoRepository.find({ views: 1 });
    console.log("All viewed photos: ", allViewedPhotos);

    let allPublishedPhotos = await photoRepository.find({ isPublished: true });
    console.log("All published photos: ", allPublishedPhotos);

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

}).catch(error => console.log(error));

Updating in the database

现在让我们从数据库加载一张照片,更新它并保存它:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photoToUpdate = await photoRepository.findOne(1);
    photoToUpdate.name = "Me, my friends and polar bears";
    await photoRepository.save(photoToUpdate);

}).catch(error => console.log(error));

现在 id = 1 的照片将在数据库中更新。

Removing from the database

现在让我们从数据库中删除我们的照片:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photoToRemove = await photoRepository.findOne(1);
    await photoRepository.remove(photoToRemove);

}).catch(error => console.log(error));

现在带有 id = 1 的照片将从数据库中删除。

Creating a one-to-one relation

让我们创建与另一个类的一对一关系。 让我们在 PhotoMetadata.ts 中创建一个新类。 这个 PhotoMetadata 类应该包含我们照片的附加元信息:

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(type => Photo)
    @JoinColumn()
    photo: Photo;
}

在这里,我们使用一个名为 @OneToOne 的新装饰器。 它允许我们在两个实体之间创建一对一的关系。 <代码>类型 => Photo 是一个函数,它返回我们想要与之建立关系的实体的类。 由于语言的特殊性,我们被迫使用返回类的函数,而不是直接使用类。 我们也可以把它写成 () => 照片,但我们使用type => 照片作为提高代码可读性的约定。 类型变量本身不包含任何内容。

我们还添加了一个 @JoinColumn 装饰器,它表示关系的这一端将拥有该关系。 关系可以是单向的或双向的。 只有关系的一面可以是拥有。 在关系的所有者端需要使用 @JoinColumn 装饰器。

如果您运行该应用程序,您将看到一个新生成的表,它将包含一个带有照片关系外键的列:

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

Save a one-to-one relation

现在让我们保存一张照片及其元数据并将它们相互关联。

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";
import {PhotoMetadata} from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    // create a photo
    let 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 a photo metadata
    let metadata = new PhotoMetadata();
    metadata.height = 640;
    metadata.width = 480;
    metadata.compressed = true;
    metadata.comment = "cybershoot";
    metadata.orientation = "portait";
    metadata.photo = photo; // this way we connect them

    // get entity repositories
    let photoRepository = connection.getRepository(Photo);
    let metadataRepository = connection.getRepository(PhotoMetadata);

    // first we should save a photo
    await photoRepository.save(photo);

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

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

}).catch(error => console.log(error));

Inverse side of the relationship

关系可以是单向的或双向的。 目前,我们的 PhotoMetadata 和 Photo 之间的关系是单向的。 关系的所有者是 PhotoMetadata,而 Photo 对 PhotoMetadata 一无所知。 这使得从照片端访问照片元数据变得复杂。 要解决这个问题,我们应该添加一个反向关系,并使 PhotoMetadata 和 Photo 之间的关系成为双向的。 让我们修改我们的实体:

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

@Entity()
export class PhotoMetadata {

    /* ... other columns */

    @OneToOne(type => 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(type => PhotoMetadata, photoMetadata => photoMetadata.photo)
    metadata: PhotoMetadata;
}

photo => photo.metadata 是一个返回关系反面名称的函数。 这里我们展示了 Photo 类的元数据属性是我们在 Photo 类中存储 PhotoMetadata 的地方。 除了传递返回照片属性的函数之外,您还可以简单地将字符串传递给 @OneToOne 装饰器,例如 "metadata"。 但是我们使用这种函数类型的方法来使我们的重构更容易。

请注意,我们应该只在关系的一侧使用 @JoinColumn 装饰器。 无论你把这个装饰器放在哪一边,都将成为关系的拥有方。 关系的拥有方包含数据库中带有外键的列。

Loading objects with their relations

现在让我们在单个查询中加载我们的照片及其照片元数据。 有两种方法 - 使用 find* 方法或使用 QueryBuilder 功能。 让我们先使用 find* 方法。 find* 方法允许您使用 FindOneOptions / FindManyOptions 接口指定一个对象。

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";
import {PhotoMetadata} from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photoRepository = connection.getRepository(Photo);
    let photos = await photoRepository.find({ relations: ["metadata"] });

}).catch(error => console.log(error));

在这里,照片将包含数据库中的一组照片,每张照片都将包含其照片元数据。 在本文档中了解有关查找选项的更多信息。

使用查找选项既好又简单,但如果您需要更复杂的查询,则应改用 QueryBuilderQueryBuilder 允许以优雅的方式使用更复杂的查询:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";
import {PhotoMetadata} from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photos = await connection
            .getRepository(Photo)
            .createQueryBuilder("photo")
            .innerJoinAndSelect("photo.metadata", "metadata")
            .getMany();


}).catch(error => console.log(error));

QueryBuilder 允许创建和执行几乎任何复杂性的 SQL 查询。 当您使用 QueryBuilder 时,请像您正在创建 SQL 查询一样思考。 在此示例中,“照片”和“元数据”是应用于所选照片的​​别名。 您使用别名来访问所选数据的列和属性。

Using cascades to automatically save related objects

我们可以在我们的关系中设置级联选项,在我们希望在保存其他对象时保存我们的相关对象的情况下。 让我们稍微更改一下照片的 @OneToOne 装饰器:

export class Photo {
    /// ... other columns

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

使用 cascade 允许我们现在不单独保存照片和单独保存元数据对象。 现在我们可以简单地保存一个照片对象,由于级联选项,元数据对象将自动保存。

createConnection(options).then(async connection => {

    // create photo object
    let 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
    let metadata = new PhotoMetadata();
    metadata.height = 640;
    metadata.width = 480;
    metadata.compressed = true;
    metadata.comment = "cybershoot";
    metadata.orientation = "portait";

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

    // get repository
    let photoRepository = connection.getRepository(Photo);

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

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

}).catch(error => console.log(error));

Creating a many-to-one / one-to-many relation

让我们创建一个多对一/一对多关系。 假设一张照片有一个作者,每个作者可以有很多张照片。 首先,让我们创建一个 Author 类:

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

@Entity()
export class Author {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

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

Author 包含关系的反面。 OneToMany 始终是关系的反面,如果没有 ManyToOne 在关系的另一边,它就不可能存在。

现在让我们将关系的所有者端添加到照片实体中:

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

@Entity()
export class Photo {

    /* ... other columns */

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

在多对一/一对多关系中,所有者端始终是多对一的。 这意味着使用 @ManyToOne 的类将存储相关对象的 ID。

运行应用程序后,ORM 将创建 author 表:

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

它还将修改 photo 表,添加一个新的 author 列并创建它的外键:

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

Creating a many-to-many relation

让我们创建一个多对一/多对多关系。 假设一张照片可以在多个相册中,每个相册可以包含很多照片。 让我们创建一个 Album 类:

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

@Entity()
export class Album {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

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

@JoinTable 需要指定这是关系的所有者方。

现在让我们添加与 Photo 类的关系的反面:

export class Photo {
    /// ... other columns

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

运行应用程序后,ORM 将创建一个albumphotosphoto_albums < em>联结表:

+-------------+--------------+----------------------------+
|                album_photos_photo_albums                |
+-------------+--------------+----------------------------+
| album_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |
| photo_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |
+-------------+--------------+----------------------------+

不要忘记在 ORM 中使用您的连接注册 Album 类:

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

现在让我们将相册和照片插入我们的数据库:

let connection = await createConnection(options);

// create a few albums
let album1 = new Album();
album1.name = "Bears";
await connection.manager.save(album1);

let album2 = new Album();
album2.name = "Me";
await connection.manager.save(album2);

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

// now our photo is saved and albums are attached to it
// now lets load them:
const loadedPhoto = await connection
    .getRepository(Photo)
    .findOne(1, { relations: ["albums"] });

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"
    }]
}

Using QueryBuilder

您可以使用 QueryBuilder 来构建几乎任何复杂性的 SQL 查询。 例如,您可以这样做:

let photos = await connection
    .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降序排列。 照片的相册将左连接,其元数据将被内连接。

您将在应用程序中经常使用查询生成器。 在此处了解有关 QueryBuilder 的更多信息。

Samples

查看sample 中的示例以获取使用示例。

您可以克隆并开始使用一些存储库:

Extensions

有几个扩展可以简化 TypeORM 的使用并将其与其他模块集成:

Contributing

了解贡献 此处 以及如何设置您的开发环境此处

这个项目的存在要感谢所有做出贡献的人:

Sponsors

开源既辛苦又费时。 如果你想投资 TypeORM 的未来,你可以成为赞助商,让我们的核心团队花更多时间在 TypeORM 的改进和新功能上。 成为赞助商

Gold Sponsors

成为金牌赞助商并从我们的核心贡献者那里获得高级技术支持。 成为金牌赞助商

TypeORM is an ORM that can run in NodeJS, Browser, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo, and Electron platforms and can be used with TypeScript and JavaScript (ES5, ES6, ES7, ES8). Its goal is to always support the latest JavaScript features and provide additional features that help you to develop any kind of application that uses databases - from small applications with a few tables to large scale enterprise applications with multiple databases.

TypeORM supports both Active Record and Data Mapper patterns, unlike all other JavaScript ORMs currently in existance, which means you can write high quality, loosely coupled, scalable, maintainable applications the most productive way.

TypeORM is highly influenced by other ORMs, such as Hibernate, Doctrine and Entity Framework.

Some TypeORM features:

  • supports both DataMapper and ActiveRecord (your choice)
  • entities and columns
  • database-specific column types
  • entity manager
  • repositories and custom repositories
  • clean object relational model
  • associations (relations)
  • eager and lazy relations
  • uni-directional, bi-directional and self-referenced relations
  • supports multiple inheritance patterns
  • cascades
  • indices
  • transactions
  • migrations and automatic migrations generation
  • connection pooling
  • replication
  • using multiple database connections
  • working with multiple databases types
  • cross-database and cross-schema queries
  • elegant-syntax, flexible and powerful QueryBuilder
  • left and inner joins
  • proper pagination for queries using joins
  • query caching
  • streaming raw results
  • logging
  • listeners and subscribers (hooks)
  • supports closure table pattern
  • schema declaration in models or separate configuration files
  • connection configuration in json / xml / yml / env formats
  • supports MySQL / MariaDB / Postgres / SQLite / Microsoft SQL Server / Oracle / sql.js
  • supports MongoDB NoSQL database
  • works in NodeJS / Browser / Ionic / Cordova / React Native / NativeScript / Expo / Electron platforms
  • TypeScript and JavaScript support
  • produced code is performant, flexible, clean and maintainable
  • follows all possible best practices
  • CLI

And more…

With TypeORM your models look like this:

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

@Entity()
export class User {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    age: number;

}

And your domain logic looks like this:

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

const allUsers = await repository.find();
const firstUser = await repository.findOne(1); // find by id
const timber = await repository.findOne({ firstName: "Timber", lastName: "Saw" });

await repository.remove(timber);

Alternatively, if you prefer to use the ActiveRecord implementation, you can use it as well:

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;

}

And your domain logic will look this way:

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

const allUsers = await User.find();
const firstUser = await User.findOne(1);
const timber = await User.findOne({ firstName: "Timber", lastName: "Saw" });

await timber.remove();

Installation

  1. Install the npm package:

    npm install typeorm --save

  2. You need to install reflect-metadata shim:

    npm install reflect-metadata --save

    and import it somewhere in the global place of your app (for example in app.ts):

    import "reflect-metadata";

  3. You may need to install node typings:

    npm install @types/node --save

  4. Install a database driver:

    • for MySQL or MariaDB

      npm install mysql --save (you can install mysql2 instead as well)

    • for PostgreSQL

      npm install pg --save

    • for SQLite

      npm install sqlite3 --save

    • for Microsoft SQL Server

      npm install mssql --save

    • for sql.js

      npm install sql.js --save

    • for Oracle

      npm install oracledb --save

      Install only one of them, depending on which database you use.

      To make the Oracle driver work, you need to follow the installation instructions from their site.

    • for MongoDB (experimental)

      npm install mongodb --save

    • for NativeScript, react-native and Cordova

      Check documentation of supported platforms

TypeScript configuration

Also, make sure you are using TypeScript compiler version 2.3 or greater, and you have enabled the following settings in tsconfig.json:

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

You may also need to enable es6 in the lib section of compiler options, or install es6-shim from @types.

Quick Start

The quickest way to get started with TypeORM is to use its CLI commands to generate a starter project. Quick start works only if you are using TypeORM in a NodeJS application. If you are using other platforms, proceed to the step-by-step guide.

First, install TypeORM globally:

npm install typeorm -g

Then go to the directory where you want to create a new project and run the command:

typeorm init --name MyProject --database mysql

Where name is the name of your project and database is the database you'll use. Database can be one of the following values: mysql, mariadb, postgres, sqlite, mssql, oracle, mongodb, cordova, react-native, expo, nativescript.

This command will generate a new project in the MyProject directory with the following files:

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

You can also run typeorm init on an existing node project, but be careful - it may override some files you already have.

The next step is to install new project dependencies:

cd MyProject
npm install

While installation is in progress, edit the ormconfig.json file and put your own database connection configuration options in there:

{
   "type": "mysql",
   "host": "localhost",
   "port": 3306,
   "username": "test",
   "password": "test",
   "database": "test",
   "synchronize": true,
   "logging": false,
   "entities": [
      "src/entity/**/*.ts"
   ],
   "migrations": [
      "src/migration/**/*.ts"
   ],
   "subscribers": [
      "src/subscriber/**/*.ts"
   ]
}

Particularly, most of the time you'll only need to configure host, username, password, database and maybe port options.

Once you finish with configuration and all node modules are installed, you can run your application:

npm start

That's it, your application should successfully run and insert a new user into the database. You can continue to work with this project and integrate other modules you need and start creating more entities.

You can generate an even more advanced project with express installed by running typeorm init --name MyProject --database mysql --express command.

Step-by-Step Guide

What are you expecting from ORM? First of all, you are expecting it will create database tables for you and find / insert / update / delete your data without the pain of having to write lots of hardly maintainable SQL queries. This guide will show you how to setup TypeORM from scratch and make it do what you are expecting from an ORM.

Create a model

Working with a database starts from creating tables. How do you tell TypeORM to create a database table? The answer is - through the models. Your models in your app are your database tables.

For example, you have a Photo model:

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

And you want to store photos in your database. To store things in the database, first you need a database table, and database tables are created from your models. Not all models, but only those you define as entities.

Create an entity

Entity is your model decorated by an @Entity decorator. A database table will be created for such models. You work with entities everywhere with TypeORM. You can load/insert/update/remove and perform other operations with them.

Let's make our Photo model as an entity:

import {Entity} from "typeorm";

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

Now, a database table will be created for the Photo entity and we'll be able to work with it anywhere in our app. We have created a database table, however what table can exist without columns? Let's create a few columns in our database table.

Adding table columns

To add database columns, you simply need to decorate an entity's properties you want to make into a column with a @Column decorator.

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;
}

Now id, name, description, filename, views and isPublished columns will be added to the photo table. Column types in the database are inferred from the property types you used, e.g. number will be converted into integer, string into varchar, boolean into bool, etc. But you can use any column type your database supports by implicitly specifying a column type into the @Column decorator.

We generated a database table with columns, but there is one thing left. Each database table must have a column with a primary key.

Creating a primary column

Each entity must have at least one primary key column. This is a requirement and you can't avoid it. To make a column a primary key, you need to use @PrimaryColumn decorator.

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;
}

Creating an auto generated column

Now, let's say you want your id column to be auto-generated (this is known as auto-increment / sequence / serial / generated identity column). To do that, you need to change the @PrimaryColumn decorator to a @PrimaryGeneratedColumn decorator:

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;
}

Column data types

Next, let's fix our data types. By default, string is mapped to a varchar(255)-like type (depending on the database type). Number is mapped to a integer-like type (depending on the database type). We don't want all our columns to be limited varchars or integers. Let's setup correct data types:

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;
}

Column types are database-specific. You can set any column type your database supports. More information on supported column types can be found here.

Creating a connection to the database

Now, when our entity is created, let's create an index.ts (or app.ts whatever you call it) file and set up our connection there:

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

createConnection({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "root",
    password: "admin",
    database: "test",
    entities: [
        Photo
    ],
    synchronize: true,
    logging: false
}).then(connection => {
    // here you can start to work with your entities
}).catch(error => console.log(error));

We are using MySQL in this example, but you can use any other supported database. To use another database, simply change the type in the options to the database type you are using: mysql, mariadb, postgres, sqlite, mssql, oracle, cordova, nativescript, react-native, expo, or mongodb. Also make sure to use your own host, port, username, password and database settings.

We added our Photo entity to the list of entities for this connection. Each entity you are using in your connection must be listed there.

Setting synchronize makes sure your entities will be synced with the database, every time you run the application.

Loading all entities from the directory

Later, when we create more entities we need to add them to the entities in our configuration. This is not very convenient, so instead we can set up the whole directory, from where all entities will be connected and used in our connection:

import {createConnection} from "typeorm";

createConnection({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "root",
    password: "admin",
    database: "test",
    entities: [
        __dirname + "/entity/*.js"
    ],
    synchronize: true,
}).then(connection => {
    // here you can start to work with your entities
}).catch(error => console.log(error));

But be careful with this approach. If you are using ts-node then you need to specify paths to .ts files instead. If you are using outDir then you'll need to specify paths to .js files inside outDir directory. If you are using outDir and when you remove or rename your entities make sure to clear outDir directory and re-compile your project again, because when you remove your source .ts files their compiled .js versions aren't removed from output directory and still are loaded by TypeORM because they present in outDir directory.

Running the application

Now if you run your index.ts, a connection with database will be initialized and a database table for your photos will be created.

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

Creating and inserting a photo into the database

Now let's create a new photo to save it in the database:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(connection => {

    let 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;

 &nbsp; &nbsp;return connection.manager
            .save(photo)
            .then(photo => {
                console.log("Photo has been saved. Photo id is", photo.id);
            });

}).catch(error => console.log(error));

Once your entity is saved it will get a newly generated id. save method returns an instance of the same object you pass to it. It's not a new copy of the object, it modifies its "id" and returns it.

Using async/await syntax

Let's take advantage of the latest ES8 (ES2017) features and use async/await syntax instead:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    let 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 connection.manager.save(photo);
    console.log("Photo has been saved");

}).catch(error => console.log(error));

Using Entity Manager

We just created a new photo and saved it in the database. We used EntityManager to save it. Using entity manager you can manipulate any entity in your app. For example, let's load our saved entity:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let savedPhotos = await connection.manager.find(Photo);
    console.log("All photos from the db: ", savedPhotos);

}).catch(error => console.log(error));

savedPhotos will be an array of Photo objects with the data loaded from the database.

Learn more about EntityManager here.

Using Repositories

Now let's refactor our code and use Repository instead of EntityManager. Each entity has its own repository which handles all operations with its entity. When you deal with entities a lot, Repositories are more convenient to use than EntityManagers:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    let 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;

    let photoRepository = connection.getRepository(Photo);

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

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

}).catch(error => console.log(error));

Learn more about Repository here.

Loading from the database

Let's try more load operations using the Repository:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let allPhotos = await photoRepository.find();
    console.log("All photos from the db: ", allPhotos);

    let firstPhoto = await photoRepository.findOne(1);
    console.log("First photo from the db: ", firstPhoto);

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

    let allViewedPhotos = await photoRepository.find({ views: 1 });
    console.log("All viewed photos: ", allViewedPhotos);

    let allPublishedPhotos = await photoRepository.find({ isPublished: true });
    console.log("All published photos: ", allPublishedPhotos);

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

}).catch(error => console.log(error));

Updating in the database

Now let's load a single photo from the database, update it and save it:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photoToUpdate = await photoRepository.findOne(1);
    photoToUpdate.name = "Me, my friends and polar bears";
    await photoRepository.save(photoToUpdate);

}).catch(error => console.log(error));

Now photo with id = 1 will be updated in the database.

Removing from the database

Now let's remove our photo from the database:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photoToRemove = await photoRepository.findOne(1);
    await photoRepository.remove(photoToRemove);

}).catch(error => console.log(error));

Now photo with id = 1 will be removed from the database.

Creating a one-to-one relation

Let's create a one-to-one relation with another class. Let's create a new class in PhotoMetadata.ts. This PhotoMetadata class is supposed to contain our photo's additional meta-information:

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(type => Photo)
    @JoinColumn()
    photo: Photo;
}

Here, we are using a new decorator called @OneToOne. It allows us to create a one-to-one relationship between two entities. type => Photo is a function that returns the class of the entity with which we want to make our relationship. We are forced to use a function that returns a class, instead of using the class directly, because of the language specifics. We can also write it as () => Photo, but we use type => Photo as a convention to increase code readability. The type variable itself does not contain anything.

We also add a @JoinColumn decorator, which indicates that this side of the relationship will own the relationship. Relations can be unidirectional or bidirectional. Only one side of relational can be owning. Using @JoinColumn decorator is required on the owner side of the relationship.

If you run the app, you'll see a newly generated table, and it will contain a column with a foreign key for the photo relation:

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

Save a one-to-one relation

Now let's save a photo, its metadata and attach them to each other.

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";
import {PhotoMetadata} from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    // create a photo
    let 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 a photo metadata
    let metadata = new PhotoMetadata();
    metadata.height = 640;
    metadata.width = 480;
    metadata.compressed = true;
    metadata.comment = "cybershoot";
    metadata.orientation = "portait";
    metadata.photo = photo; // this way we connect them

    // get entity repositories
    let photoRepository = connection.getRepository(Photo);
    let metadataRepository = connection.getRepository(PhotoMetadata);

    // first we should save a photo
    await photoRepository.save(photo);

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

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

}).catch(error => console.log(error));

Inverse side of the relationship

Relations can be unidirectional or bidirectional. Currently, our relation between PhotoMetadata and Photo is unidirectional. The owner of the relation is PhotoMetadata, and Photo doesn't know anything about PhotoMetadata. This makes it complicated to access PhotoMetadata from the Photo side. To fix this issue we should add an inverse relation, and make relations between PhotoMetadata and Photo bidirectional. Let's modify our entities:

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

@Entity()
export class PhotoMetadata {

    /* ... other columns */

    @OneToOne(type => 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(type => PhotoMetadata, photoMetadata => photoMetadata.photo)
    metadata: PhotoMetadata;
}

photo => photo.metadata is a function that returns the name of the inverse side of the relation. Here we show that the metadata property of the Photo class is where we store PhotoMetadata in the Photo class. Instead of passing a function that returns a property of the photo, you could alternatively simply pass a string to @OneToOne decorator, like "metadata". But we used this function-typed approach to make our refactoring easier.

Note that we should use @JoinColumn decorator only on one side of a relation. Whichever side you put this decorator on will be the owning side of the relationship. The owning side of a relationship contains a column with a foreign key in the database.

Loading objects with their relations

Now let's load our photo and its photo metadata in a single query. There are two ways to do it - using find* methods or using QueryBuilder functionality. Let's use find* methods first. find* methods allow you to specify an object with the FindOneOptions / FindManyOptions interface.

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";
import {PhotoMetadata} from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photoRepository = connection.getRepository(Photo);
    let photos = await photoRepository.find({ relations: ["metadata"] });

}).catch(error => console.log(error));

Here, photos will contain an array of photos from the database, and each photo will contain its photo metadata. Learn more about Find Options in this documentation.

Using find options is good and dead simple, but if you need a more complex query, you should use QueryBuilder instead. QueryBuilder allows more complex queries to be used in an elegant way:

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";
import {PhotoMetadata} from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photos = await connection
            .getRepository(Photo)
            .createQueryBuilder("photo")
            .innerJoinAndSelect("photo.metadata", "metadata")
            .getMany();


}).catch(error => console.log(error));

QueryBuilder allows creation and execution of SQL queries of almost any complexity. When you work with QueryBuilder, think like you are creating an SQL query. In this example, "photo" and "metadata" are aliases applied to selected photos. You use aliases to access columns and properties of the selected data.

Using cascades to automatically save related objects

We can setup cascade options in our relations, in the cases when we want our related object to be saved whenever the other object is saved. Let's change our photo's @OneToOne decorator a bit:

export class Photo {
    /// ... other columns

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

Using cascade allows us not to separately save photo and separately save metadata objects now. Now we can simply save a photo object, and the metadata object will be saved automatically because of cascade options.

createConnection(options).then(async connection => {

    // create photo object
    let 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
    let metadata = new PhotoMetadata();
    metadata.height = 640;
    metadata.width = 480;
    metadata.compressed = true;
    metadata.comment = "cybershoot";
    metadata.orientation = "portait";

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

    // get repository
    let photoRepository = connection.getRepository(Photo);

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

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

}).catch(error => console.log(error));

Creating a many-to-one / one-to-many relation

Let's create a many-to-one / one-to-many relation. Let's say a photo has one author, and each author can have many photos. First, let's create an Author class:

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

@Entity()
export class Author {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

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

Author contains an inverse side of a relation. OneToMany is always an inverse side of relation, and it can't exist without ManyToOne on the other side of the relation.

Now let's add the owner side of the relation into the Photo entity:

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

@Entity()
export class Photo {

    /* ... other columns */

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

In many-to-one / one-to-many relation, the owner side is always many-to-one. It means that the class that uses @ManyToOne will store the id of the related object.

After you run the application, the ORM will create the author table:

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

It will also modify the photo table, adding a new author column and creating a foreign key for it:

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

Creating a many-to-many relation

Let's create a many-to-one / many-to-many relation. Let's say a photo can be in many albums, and each album can contain many photos. Let's create an Album class:

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

@Entity()
export class Album {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

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

@JoinTable is required to specify that this is the owner side of the relationship.

Now let's add the inverse side of our relation to the Photo class:

export class Photo {
    /// ... other columns

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

After you run the application, the ORM will create a albumphotosphoto_albums junction table:

+-------------+--------------+----------------------------+
|                album_photos_photo_albums                |
+-------------+--------------+----------------------------+
| album_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |
| photo_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |
+-------------+--------------+----------------------------+

Don't forget to register the Album class with your connection in the ORM:

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

Now let's insert albums and photos to our database:

let connection = await createConnection(options);

// create a few albums
let album1 = new Album();
album1.name = "Bears";
await connection.manager.save(album1);

let album2 = new Album();
album2.name = "Me";
await connection.manager.save(album2);

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

// now our photo is saved and albums are attached to it
// now lets load them:
const loadedPhoto = await connection
    .getRepository(Photo)
    .findOne(1, { relations: ["albums"] });

loadedPhoto will be equal to:

{
    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"
    }]
}

Using QueryBuilder

You can use QueryBuilder to build SQL queries of almost any complexity. For example, you can do this:

let photos = await connection
    .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();

This query selects all published photos with "My" or "Mishka" names. It will select results from position 5 (pagination offset), and will select only 10 results (pagination limit). The selection result will be ordered by id in descending order. The photo's albums will be left-joined and their metadata will be inner joined.

You'll use the query builder in your application a lot. Learn more about QueryBuilder here.

Samples

Take a look at the samples in sample for examples of usage.

There are a few repositories which you can clone and start with:

Extensions

There are several extensions that simplify working with TypeORM and integrating it with other modules:

Contributing

Learn about contribution here and how to setup your development environment here.

This project exists thanks to all the people who contribute:

Sponsors

Open source is hard and time-consuming. If you want to invest into TypeORM's future you can become a sponsor and make our core team to spend more time on TypeORM's improvements and new features. Become a sponsor

Gold Sponsors

Become a gold sponsor and get a premium technical support from our core contributors. Become a gold sponsor

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文