【译】关系对象模型 typeorm 上

typeorm 有什么用?typeorm 以操作对象方法的方式与数据库交互,而不是像传统库(mysql)那样需要写 sql 语句。

本文主要说什么?Typeorm README.md 写的很长,往下拉可以看到 "Step-by-Step Guide" 标题,本文翻译这部分内容(非直译),帮助新手一步步熟悉并使用 typeorm。(翻译时 typeorm 版本为 0.2.24。)

准备工作

初始化工程

$ mkdir test-typeorm
$ cd test-typeorm
$ npm init -y

安装依赖包

$ npm install typeorm --save
$ npm install reflect-metadata --save
$ npm install mysql --save
$ npm install typescript --save-dev
$ npm install @types/node --save-dev
  • typeormreflect-metadata 是使用 typeorm 必备依赖;
  • 这里使用 mysql 数据库,所以还要安装 mysql 依赖;
  • 要用到装饰器,typescript@types/node 是使用 typescript 必备依赖。

搭建 ts 环境

创建 tsconfig.json 配置文件

$ npx tsc --init

修改 tsconfig.json 文件中部分属性

"outDir": "./dist",
"rootDir": "./src",
"strictPropertyInitialization": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,

创建 src 目录,源码都写在该目录中,之后目录结构如下:

|-- test-typeorm
    |-- src
    |-- package.json
    |-- tsconfig.json

修改 package.json 文件:

"scripts": {
    "dev": "tsc -w"
}

测试

创建文件 src/test.ts,内容如下:

const greet: string = 'hello typeorm'
console.log(greet)

打开终端,执行 $ npm run dev,可以看到根目录下生成了 dist 目录,里面包含编译后的 js 文件。

另打开一个终端,执行 $ node dist/test.js,可以看到控制台打印出问候 "hello typeorm"。

至此,依赖包和环境搭建完成,下面开始进入正题,先来学习如何使用 typeorm 创建数据库表。

Typeorm 生成数据库表

创建 model

与数据库打交道的第一步是需要创建一张数据库表,TypeORM 怎么知道你要创建什么样的表呢?答案是通过 model 类,你的程序代码中的 model 类就是你的数据库表。

创建 src/entity/Photo.ts 文件,内容如下:

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

你想往数据库里存一些照片,为了做到这个,你首先需要建一张数据库表,typeorm 会根据你的 model 类自动创建表,但并非所有的 models 类都可以创建表,只有那些实现了 entity 的 model 类才有这个特性。

此时您的目录结构如下:

|-- test-typeorm
    |-- src
        |-- entity
            |-- Photo.ts
    |-- package.json
    |-- tsconfig.json

创建 entity

为你的 model 类添加 @Entity 装饰器,只有符合这种规则的 model 类,typeorm 才会帮助你建表。

将我们的 Photo model 改造成 Photo entity。

import { Entity } from "typeorm";

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

现在,表已经创立了,但是表中还没有字段,让我们为数据库表创建一些字段。

创建表字段

要为表添加字段,您只需要给 entity 类的属性值添加 @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 表中了。 数据库里的列类型会根据 entity 类中字段类型自动转换,例如:类中的 number 类型会转换为数据库列类型为 integer,string 转换为 varchar,boolean 转换为 bool 等等。您还可以设置 @Column() 装饰器的参数明确设置数据库列类型。

@Column('varchar2')
filename: string;

我们已经为数据库表添加了字段,但还有件事没有做,每张表应该有一个主键字段。

创建主键列

每个 Entity 类至少有一个主键字段,这是 typeorm 规定的,并且是不可避免的。使用 @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 的值不能重复。

创建主键自生成字段

您已经创建了主键列,每次存数据时需要手动添加 id 字段的值,如果每次插值时想要自动生成主键值,使用 @PrimaryGeneratedColumn 装饰器代替 @PrimaryColumn 装饰器。

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

@Entity()
export class Photo {

    @PrimaryGeneratedColumn('increment')    // 主键自增长
    // @PrimaryGeneratedColumn('uuid')      // 自动生成 uuid 作为主键:'0ae5e5c8-36f3-4b7b-84e4-c0b79cdfb1d1'
    id: number;

    @Column()
    name: string;

    @Column()
    description: string;

    @Column()
    filename: string;

    @Column()
    views: number;

    @Column()
    isPublished: boolean;
}

完善类类型

接下来,让我们完善数据库类类型。默认情况,string 被转换为 varchar 类型,长度自动设置为 255,number 被转换为 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;
}

创建数据库连接

准备就绪,让我们尝试连接数据库。

创建 src/testConnect.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    // <==== 将需要操作的 Entity 导入进来
    ],
    synchronize: true,
    logging: false
}).then(connection => {
    // here you can start to work with your entities
    console.log('数据库连接成功,做点什么吧~')
}).catch(error => console.log(error));

将数据库连接信息改成您自己数据库的相关值:host、port、username、password、database

在这个例子中我们使用 MySQL 数据库,您也可以自由选择使用其它数据库。要用其它数据库,只需要简单的修改 type 属性,值可以是:mysqlmariadbsqlitemssqloraclemongodb

我们将 Photo Entity 类添加到此次连接中,您需要将使用的所有 Entity 都列出来。

设置 synchronize 属性为 true,确保每次运行应用时,您的 Entity 实体类将与数据库同步。
(译者注:Entity 中新增了一个字段或修改了字段的数据类型,每次启动应用,数据库表也会同步这些修改)

加载所有的 Entities

当我们创建更多 Entity 实体类时,需要我们每次都手动将 Entity 文件添加到配置中的 entities 属性,这很不方便,您可以直接导入整个 entity 目录。

import { createConnection } from "typeorm";

createConnection({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "root",
    password: "admin",
    database: "test",
    entities: [
        __dirname + "/entity/*.js"   // <=== 将 entity 目录下所有文件全导进来
    ],
    synchronize: true,
}).then(connection => {
    // here you can start to work with your entities
    console.log('数据库连接成功,做点什么吧~')
}).catch(error => console.log(error));

(译者注:如果 Vscode 报错 __dirname 未定义,安装 @types/node 包即可解决。 )

运行应用

执行 $ node dist/testConnect.js,数据库连接会被初始化,去查看您的数据库,photo 表格已经被创建了。

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

Typeorm 增删改查

新增数据

让我们创建一张图片并保存到数据库中,创建 src/testPhotoCurd.ts 文件,内容如下:
(createConnection() 方法的连接数据库的参数和上面是一样样的,这里用 /.../ 替代)**

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;

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

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

执行 $ node dist/testPhotoCurd.js,查看数据库,已经生成一张照片数据了。

新增操作会自动生成 id,save() 方法返回的对象和你传给它的对象是同一个对象,唯一的区别是为 photo 对象设置了 id 字段值并返回。

使用 async/await 语法

使用 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));

执行 $ node dist/testPhotoCurd.js,查看数据库又生成一条数据。
(下面如没有特别说明,代码都是写在 src/testPhotoCurd.ts 文件中)

使用 Entity Manager

前面使用 EntityManager 我们已经创建了两张图片并保存到数据库中。
使用 EntityManager,您可以操作应用中的任何 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 是从数据库取出来的 photo 对象组成的数组。

使用 Repositories

Repository 也可以做到 EntityManager 做的事,让我们使用 Repository 重构上面的代码。
当你要对同一个 Entity 做多次处理时,Repositories 会更加方便些。

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);     // <=== 操作1:保存数据
    console.log("Photo has been saved");

    let savedPhotos = await photoRepository.find();   // <=== 操作2:查询数据
    console.log("All photos from the db: ", savedPhotos);  

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

更新数据

首先从数据库把数据查出来,然后修改它的值。

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

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

    /*...*/
    let photoRepository = connection.getRepository(Photo); 

    let photoToUpdate = await photoRepository.findOne(1);   // <=== 操作1:查询数据
    if (photoToUpdate !== undefined) {
        photoToUpdate.name = "Me, my friends and polar bears";
        await photoRepository.save(photoToUpdate);      // <=== 操作2:修改数据
    }

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

现在,id = 1 的数据被修改了。

删除数据

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

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

    /*...*/
    let photoRepository = connection.getRepository(Photo);

    let photoToRemove = await photoRepository.findOne(1);   // <=== 操作1:查询数据
    if (photoToRemove !== undefined) {
        await photoRepository.remove(photoToRemove);        // <=== 操作2:修改数据
    }

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

现在,id = 1 的数据被删除了。

查询数据

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

    /* 查询 id = 1 的第一条数据 */
    let firstPhoto = await photoRepository.findOne(1);
    console.log("First photo from the db: ", firstPhoto);

    /* 查询 name = 'Me and Bears' 的第一条数据 */
    let meAndBearsPhoto = await photoRepository.findOne({ name: "Me and Bears" });
    console.log("Me and Bears photo from the db: ", meAndBearsPhoto);

    /* 查询 views = 1 的所有数据 */
    let allViewedPhotos = await photoRepository.find({ views: 1 });
    console.log("All viewed photos: ", allViewedPhotos);

    /* 查询 isPublished = true 的所有数据 */
    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));

更复杂的查询数据

您可以使用 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 的数据开始取(pagination offset),只取 10 条数据(pagination limit),检索到的数据按照 photo.id 倒序排列,photo 表左连接 albums 表,内连接 metadata 表查询。

推荐阅读更多精彩内容