diff --git a/docs/associations.md b/docs/associations.md index f10026cf..0de590a4 100644 --- a/docs/associations.md +++ b/docs/associations.md @@ -37,6 +37,22 @@ Leoric supports four types of associations: Associations can be declared within the `Model.describe()` method. For example, by declaring a shop `belongsTo()` its owner, you're telling Leoric that when `Shop.find().with('owner')`, Leoric should join the table of owners, load the data, and instantiate `shop.owner` on the found objects. +There are four equivalent decorators for projects written in TypeScript + +- `@BelongsTo()` +- `@HasMany()` +- `@HasMany({ through })` +- `@HasOne()` + +The major difference between static method and decorators for associations is that the first parameter can be omitted in the decorator equivalent. For example, `Post.belongsTo('user')` declared with decorator is like below: + +```ts +class Post { + @BelongsTo() + user: User +} +``` + ### `belongsTo()`
@@ -53,6 +69,15 @@ class Item extends Bone { } ``` +The TypeScript equivalent with decorator is like below: + +```ts +class Item extends Bone { + @BelongsTo() + shop: Shop; +} +``` + Leoric locates the model class `Shop` automatically by capitalizing `shop` as the model name. If that's not the case, we can specify the model name explicitly by passing `className`: ```js @@ -63,6 +88,15 @@ class Item extends Bone { } ``` +The TypeScript equivalent with decorator is like below: + +```ts +class Item extends Bone { + @BelongsTo({ className: 'Seller' }) + shop: Shop; +} +``` + > Please be noted that the value passed to `className` is a string rather than the actual model class. Tossing the actual classes back and forth between the two parties of an association at `Model.describe()` phase can be error prone because it causes cyclic dependencies. As you can tell from the ER diagram, the foreign key used to associate a `belongsTo()` relationship is located on the model that initiates it. The name of the foreign key is found by uncapitalizing the target model's name and then appending an `Id`. In this case, the foerign key is converted from `Shop` to `shopId`. @@ -79,6 +113,15 @@ class Item extends Bone { } ``` +The TypeScript equivalent with decorator is like below: + +```ts +class Item extends Bone { + @BelongsTo({ foreignKey: 'sellerId' }) + shop: Shop; +} +``` + ### `hasMany()`
@@ -95,6 +138,15 @@ class Shop extends Bone { } ``` +The TypeScript equivalent with decorator is like below: + +```ts +class Shop extends Bone { + @HasMany() + items: Item[]; +} +``` + > Please be noted that unlike `belongsTo()`, the name passed to `hasMany()` is usually in plural. The way Leoric locates the actual model class is quite similar. It starts with singularizing the name, then capitalizing. In this case, `items` get singularized to `item`, and then `Item` is used to look for the actual model class. @@ -109,6 +161,16 @@ class Shop extends Bone { } ``` +The TypeScript equivalent with decorator is like below: + +```ts +class Shop extends Bone { + // It might be able to deduce the className from `Commodify[]` type + @HasMany({ className: 'Commodity' }) + items: Commodity[]; +} +``` + As you can tell from the ER diagram, the foreign key used to join two tables is located at the target table, `items`. To override the foreign key, just pass it to the option of `hasMany()`: ```js @@ -119,6 +181,16 @@ class Shop extends Bone { } ``` +The TypeScript equivalent with decorator is like below: + +```ts +class Shop extends Bone { + @HasMany({ foreignKey: 'sellerId' }) + items: Item[]; +} +``` + + ### `hasMany({ through })` The world of entity relationships doesn't consist of one-to-one or one-to-many associations only. There are many scenarios that require a many-to-many association to be setup. However, in relational databases many-to-many between two tables isn't possible by nature. To accompish this, we need to introduce an intermediate table to bridge the associations. @@ -145,6 +217,18 @@ class Shop extends Bone { } ``` +The TypeScript equivalent with decorator is like below: + +```ts +class Shop extends Bone { + @HasMany({ foreignKey: 'targetId', where: { targetType: 0 } }) + tagMaps: TagMap[]; + + @HasMany({ through: 'tagMaps' }) + tags: Tag[]; +} +``` + On `Tag`'s side: ```js @@ -156,6 +240,18 @@ class Tag extends Bone { } ``` +The TypeScript equivalent with decorator is like below: + +```ts +class Shop extends Bone { + @HasMany({ className: 'TagMap', foreignKey: 'targetId', where: { targetType: 0 } }) + shopTagMaps: TagMap[]; + + @HasMany({ through: 'shopTagMaps' }) + shops: Tag[]; +} +``` + If suddenly our business requires us to apply the tag system to items too, the changes needed on `Tag` model is trivial: ```diff @@ -191,6 +287,15 @@ class User extends Bone { } ``` +The TypeScript equivalent with decorator is like below: + +```ts +class User extends Bone { + @HasOne({ foreignKey: 'ownerId' }) + shop: Shop; +} +``` + And the shop belongs to the user: ```js @@ -201,6 +306,15 @@ class Shop extends Bone { } ``` +The TypeScript equivalent with decorator is like below: + +```ts +class Shop extends Bone { + @BelongsTo({ className: 'User' }) + owner: User; +} +``` + ### Choosing Between `belongsTo()` and `hasOne()` As dicussed in the `hasOne()` section, the difference between `belongsTo()` and `hasOne()` is mostly at where to place the foreign key. The corresponding model of the table that contains the foreign key should be the one that declares the `belongsTo()` association. diff --git a/docs/basics.md b/docs/basics.md index 60bcda6e..2004c293 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -157,7 +157,14 @@ class Shop extends Bone { } ``` -A lot of schema settings can be done within the `static initialize()` method. We'll get to that later. +A lot of schema settings can be done within the `static initialize()` method. We'll get to that later. For TypeScript projects this static method is unnecessary, most of the settings can be tweaked with the equivalent decorators. The example above can be refactored with decorator like below: + +```ts +class Shop extends Bone { + @Column({ name: 'removed_at' }) + deltedAt: Date; +} +``` ## Connecting Models to Database diff --git a/docs/types.md b/docs/types.md index 3d57c53e..dcf80489 100644 --- a/docs/types.md +++ b/docs/types.md @@ -41,7 +41,8 @@ Here is the list of options supported by `@Column()` that can be used to customi | primaryKey = false | declare class field as the primary key | | autoIncrement = false | enable auto increment on corresponding class field, must be numeric type | | allowNull = true | class field can not be null when persisting to database | -| type = typeof field | override the data type deduced from class field type | +| type = typeof field | override the data type deduced from class field type | +| name = string | actual name of the table field in database | If `type` option is omitted, `@Column()` will try to deduce the corresponding one as below: @@ -53,6 +54,18 @@ If `type` option is omitted, `@Column()` will try to deduce the corresponding on | bigint | BIGINT | | boolean | BOOLEAN / TINYINT(1) | +Here is an example that is a little bit more comprehensive: + +```ts +class User extends Bone { + @Column({ name: 'ssn', primaryKey: true, type: VARCHAR(16) }) + ssn: string; + + @Column({ name: 'gmt_create', allowNull: false }) + createdAt: Date; +} +``` + ### BelongsTo ```ts diff --git a/docs/zh/associations.md b/docs/zh/associations.md index 7489be74..9b5a3baf 100644 --- a/docs/zh/associations.md +++ b/docs/zh/associations.md @@ -37,6 +37,22 @@ Leoric 支持四种关联关系: 这些方法需要在 `Model.describe()` 方法中调用。例如,声明店铺属于 `belongsTo()` 它的 `owner` 之后,Leoric 将在执行 `Shop.find().with('owner')` 时自动 JOIN 店铺和用户表,找到所查找的店铺对应的 `owner`,在结果中实例化对应的数据模型并挂载到店铺的 `owner` 属性。 +使用 TypeScript 的项目也可以用更直观的装饰器配置方式,上述四种关联关系都有对应的装饰器: + +- `@BelongsTo()` +- `@HasMany()` +- `@HasMany({ through })` +- `@HasOne()` + +和静态方法的区别主要是第一个参数不需要指定关联关系名称,其余基本一致,例如 `Post.belongsTo('user')` 对应的装饰器是: + +```ts +class Post { + @BelongsTo() + user: User +} +``` + ### `belongsTo()`
@@ -53,6 +69,15 @@ class Item extends Bone { } ``` +或者使用对应的装饰器来声明关联关系: + +```ts +class Item extends Bone { + @BelongsTo() + shop: Shop; +} +``` + Leoric 会把关联关系的名称 `shop` 转为驼峰、首字母大写,再以转换后的 `Shop` 为数据模型名称寻找对应的数据模型定义。如果实际的数据模型名称并非如此,我们也可以使用 `className` 显式指定: ```js @@ -63,6 +88,15 @@ class Item extends Bone { } ``` +使用对应的装饰器: + +```ts +class Item extends Bone { + @BelongsTo({ className: 'Seller' }) + shop: Shop; +} +``` + > 注意传给 `className` 的值是字符串而非实际数据模型的类。在 `Model.describe()` 定义阶段互相传递数据模型的类很容易导致循环依赖,以至于 `require` 到不一致的 `exports`。 如你在实例关系图中所见,用于关联 `belongsTo()` 关系的外键是存在于发起关联关系的数据模型中的。外键的名称默认根据目标数据模型的名称转换,首字母转为小写,再跟上 `Id` 后缀。在这个例子里,外键会自动根据 `Shop` 转换成 `shopId`。 @@ -79,6 +113,15 @@ class Item extends Bone { } ``` +使用对应的装饰器: + +```ts +class Item extends Bone { + @BelongsTo({ foreignKey: 'sellerId' }) + shop: Shop; +} +``` + ### `hasMany()`
@@ -95,6 +138,15 @@ class Shop extends Bone { } ``` +使用对应的装饰器: + +```ts +class Shop extends Bone { + @HasMany() + items: Item[]; +} +``` + > 注意,与 `belongsTo()` 不同的是,传给 `hasMany()` 的名称通常是复数形式。 Leoric 寻找对应数据模型的方式都是差不多的。首先将关联关系的名称转为单数,继而首字母大写。在此例中,`items` 被转为 `item`,继而使用 `Item` 寻找实际的数据模型类。 @@ -109,6 +161,16 @@ class Shop extends Bone { } ``` +使用对应的装饰器: + +```ts +class Shop extends Bone { + // 一般可以通过类型名识别出对应的 className + @HasMany({ className: 'Commodity' }) + items: Commodity[]; +} +``` + 如你在实例关系图所见,`hasMany()` 的外键是在目标数据模型对应的表 `items` 中的。要覆盖默认的外键名称,给 `hasMany()` 传 `foreignKey` 即可: ```js @@ -119,6 +181,15 @@ class Shop extends Bone { } ``` +使用对应的装饰器: + +```ts +class Shop extends Bone { + @HasMany({ foreignKey: 'sellerId' }) + items: Item[]; +} +``` + ### `hasMany({ through })` 数据库实例关系的世界并不仅由一对一或者一对多两种关联关系组成。实际业务中存在大量需要多对多的关联关系需要配置。但是,在关系型数据库中多对多的关联关系没办法仅使用两个表实现。为实现这一特性,我们需要引入一张中间表来记录多对多的关系。 @@ -145,17 +216,45 @@ class Shop extends Bone { } ``` +使用对应的装饰器: + +```ts +class Shop extends Bone { + @HasMany({ foreignKey: 'targetId', where: { targetType: 0 } }) + tagMaps: TagMap[]; + + @HasMany({ through: 'tagMaps' }) + tags: Tag[]; +} +``` + 在 `Tag` 这边则是: ```js class Tag extends Bone { static initialize() { - this.hasMany('shopTagMaps', { className: 'TagMap', where: { targetType: 0 } }) + this.hasMany('shopTagMaps', { + className: 'TagMap', + foreignKey: 'targetId', + where: { targetType: 0 }, + }) this.hasMany('shops', { through: 'shopTagMaps' }) } } ``` +使用对应的装饰器: + +```ts +class Shop extends Bone { + @HasMany({ className: 'TagMap', foreignKey: 'targetId', where: { targetType: 0 } }) + shopTagMaps: TagMap[]; + + @HasMany({ through: 'shopTagMaps' }) + shops: Tag[]; +} +``` + 假设需求有变,我们需要给商品也增加标签系统,我们在 `Tag` 数据模型这边只需稍许改动: ```diff @@ -191,6 +290,15 @@ class User extends Bone { } ``` +使用对应的装饰器: + +```ts +class User extends Bone { + @HasOne({ foreignKey: 'ownerId' }) + shop: Shop; +} +``` + 而店铺与用户也是一对一的关系: ```js @@ -201,6 +309,15 @@ class Shop extends Bone { } ``` +使用对应的装饰器: + +```ts +class Shop extends Bone { + @BelongsTo({ className: 'User' }) + owner: User; +} +``` + ### 在 `belongsTo()` 和 `hasOne()` 之间选择 正如 `hasOne()` 章节所讨论的,`belongsTo()` 和 `hasOne()` 之间的区别主要在外键应该在哪个数据模型。持有相应外键的数据模型应当是发起 `belongsTo()` 关联关系的一方。 diff --git a/docs/zh/basics.md b/docs/zh/basics.md index 763cf947..4875cbf8 100644 --- a/docs/zh/basics.md +++ b/docs/zh/basics.md @@ -109,6 +109,24 @@ const realm = new Realm({ host: 'localhost', models: [ Shop ] }); await realm.sync(); ``` +如果你的项目使用 TypeScript 编写,也可以使用装饰器来声明模型: + +```ts +import { Bone, Realm } from 'leoric'; +const { BIGINT, STRING } = Bone.DataTypes; + +// define Shop +class Shop extends Bone { + // 主键声明也可以省略,默认按照如下方式声明 + @Column({ primaryKey: true }) + id: bigint; + + // 字符串默认按照 VARCHAR(255) 类型定义 + @Column() + name: string; +} +``` + 然后就可以用 `Shop` 数据模型操作数据了: ```js @@ -132,7 +150,7 @@ class Shop extends Bone { ```js class Shop extends Bone { - static primaryKey = 'shopId' } + static primaryKey = 'shopId' } ``` @@ -156,7 +174,14 @@ class Shop extends Bone { } ``` -`static initialize()` 方法中可配置的项目有很多。我们之后再详细讨论。 +还可以在 `static initialize()` 中配置模型的关联关系,具体方法会在之后详细讨论。TypeScript 项目一般不需要通过这个静态方法,相关配置都有提供对应的装饰器版本,上述示例对应的 TypeScript 声明方式为: + +```ts +class Shop extends Bone { + @Column({ name: 'removed_at' }) + deltedAt: Date; +} +``` ## 连接数据模型和数据库 @@ -402,6 +427,29 @@ class Shop extends Bone { } ``` +使用装饰器的版本: + +```ts +class Shop extends Bone { + @Column() + name: STRING; + + @Column() + createdAt: DATE; + + @Column() + updatedAt: DATE; + + @Column({ type: STRING }) + set name(value) { + if ([ 'FamilyMart', '7-Eleven' ].includes(value)) { + this.attribute('name', value); + } + throw new Error(`unknown shop name: ${value}`); + } +} +``` + 此时仍然可以直接读取 `name`: ```js diff --git a/docs/zh/types.md b/docs/zh/types.md index 01a77702..6bd98bfa 100644 --- a/docs/zh/types.md +++ b/docs/zh/types.md @@ -41,7 +41,8 @@ class User extends Bone { | primaryKey = false | 声明主键 | | autoIncrement = false | 启用自增字段,字段类型必须是数值类型 | | allowNull = true | 允许字段存储空值 NULL | -| type = typeof field | 自定义字段类型 | +| type = typeof field | 自定义字段类型 | +| name = string | 原始字段名 | 如果省略 `type` 配置项,`@Column()` 会尝试按照如下映射关系推导当前字段类型: @@ -53,6 +54,18 @@ class User extends Bone { | bigint | BIGINT | | boolean | BOOLEAN / TINYINT(1) | +一个比较复杂的例子: + +```ts +class User extends Bone { + @Column({ name: 'ssn', primaryKey: true, type: VARCHAR(16) }) + ssn: string; + + @Column({ name: 'gmt_create', allowNull: false }) + createdAt: Date; +} +``` + ### BelongsTo ```ts