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
- typeorm、reflect-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;
}
现在,id
、name
、description
、filename
、views
和 isPublished
列已经被添加到 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 属性,值可以是:mysql
、mariadb
、sqlite
、mssql
、oracle
或 mongodb
。
我们将 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 表查询。