用于前端的实体关系框架, 解决存在关联关系的实体之间的CRUD
实体是用于描述一种数据的数据结构, 实体的实例在Entity Framework(以后简称为EF)中定义成一个class的实例, 而非原生的JSON数据, 例如:
// Wrong ❌
const foo = { id: 1, name: 'foo' }
// Right ✅
class Foo {
id: number
name: string
}
const foo = new Foo()
EF中, 采用注解来描述代表实体数据结构的class, 目前一共有二类注解
类注解
@behavior('load', `${domain}/foo/$id`, 'GET')
class Foo {
// define ...
}
完整签名查看源代码 src/annotations/object/behavior.ts
@behavior('load', `http://localhost:3000/foo/$id`, 'GET')
class Foo {
@primary()
id: number
}
const ctx = new Context()
// 加载主键为1的Foo的数据
await ctx.foo.load(1)
// 在调用load方法时, 会向 http://localhost:3000/foo/$id 发起 GET 请求, 并在请求前将 $id 替换为 1
属性注解
class Bar {
// define ...
}
class Foo {
@primary()
id: number
@member()
name: string
@foreign(Bar, 'bar')
bid: number
@navigator(Relationship.One, 'bar')
bar?: Bar
}
完整签名查看源代码 src/annotations/property/primary.ts
完整签名查看源代码 src/annotations/property/member.ts
完整签名查看源代码 src/annotations/property/foreign.ts
完整签名查看源代码 src/annotations/property/navigator.ts
参考数据库设计, 可以用主外键来描述, 并部署相关的导航属性, 其中导航名称(navigatorName)是很重要的一个数据, 用来联系@set(), @foreign(), @navigator()标记的数据, 代码标记[1][2][3]处须一致
class Bar {
@primary()
id: number = 0
}
class Haz {
@primary()
id: number = 0
}
class Foo {
@primary()
id: number = 0
// Foo 与 Bar 是一对一关联
@foreign(Bar, 'bar-navigatorName') // <- [1]
bid: number = 0
// Foo 与 Haz 是一对多关联
@foreign(Haz, 'haz')
hid: number[] = [0]
@navigator(Relationship.One, 'bar-navigatorName') // <- [2]
bar?: Bar
@navigator(Relationship.Many, 'haz')
haz?: Haz[]
}
class Context extends EntityContext {
@set('bar-navigatorName') // <- [3]
bar = new EntitySet<Bar>(this, Bar)
}
EntitySet 是用来存储实体的容器, 内部用Set来存储数据, EntitySet字段的名称就是导航名称(navigatorName)
EntityContext 是用来界定相关数据范围的
// 定义一个 Context
class MyContext extends EntityContext {
@set()
foo = new EntitySet<Foo>(this, Foo)
@set()
bar = new EntitySet<Bar>(this, Bar)
@set()
haz = new EntitySet<Haz>(this, Haz)
}
// 实例化 EntityContext
const ctx = new MyContext()
加载数据是对数据进行各种操作的前提, 加载数据有二种方式, 用于应对二种情况
@behavior('load', 'http://localhost:3000/foo/$pk', 'GET')
@behavior('loadAll', 'http://localhost:3000/foo', 'GET')
class Foo {
// define ...
}
Load 通过定义的主键(或者组合键)唯一实体数据, 需要先部署Load behavior
// 加载只有一个主键的数据
// http://localhost:3000/foo/$pk -> http://localhost:3000/foo/1
await ctx.foo.load(1)
// 加载部署了组合键的数据
// http://localhost:3000/foo/$pk1/$pk2 -> http://localhost:3000/foo/1/2
// 参数顺序为描述模型中@primary标记的顺序
await ctx.foo.load(1, 2)
LoadAll 通过传入的条件加载所有符合条件的数据
// 加载符合条件的所有数据
await ctx.foo.loadAll({
// anything
})
加载数据的副作用
每一次Load或者LoadAll, 都会在不清除上一次加载的数据的情况下, 添加新的数据, 如果不想被上一次的数据干扰, 可以使用clean方法
const ctx = new YourContext()
await ctx.foo.load(1)
await ctx.foo.load(1)
// 此时ctx.foo.size 为 2, 因为加载了二次
ctx.clean()
// 此时ctx.foo.size 为 1, 清除了之前加载的数据
// 或者每次加载之前执行 clean
await ctx.clean().foo.load(1)
查询参数到RequestBody的映射 与 ResponseBody到实体数据的映射, 参见@behavior注解的mapParameters和mapEntity参数
完整源代码参见 src/entitySet.ts
// 在加载主键为 1 的Foo的数据时, 将与Foo相关的Bar的数据也一并加载, 若Foo与Bar为一对一关系, EF会发起二个请求, 在Foo请求正确完成之后, 再发起对Bar的请求
await ctx.foo.include('bar').load(1)
// 在加载完符合条件的Foo数据后, 将与Foo相关的Bar的数据也一并加载, 若Foo存在十条数据, 且Foo与Bar为一对一关系, EF会再并行发送十个请求, 用于加载对应的Bar数据
await ctx.foo.include('bar').loadAll()
await ctx.foo.rawFetch(() => window.fetch('/bar').then(res => res.json()))
在EF完成对数据的加载后, 就可以直接查询数据, 查询数据有二种方式, 分别应对二种情况
const foo: Foo = ctx.foo.find(1)
// or
const foo: Foo = ctx.foo.find(1, 2)
const foo: Foo[] = ctx.foo.filter((n) => n.id === 1 || n.id === 2)
// Add entity
const newFoo = new Foo()
// newFoo 赋值
ctx.foo.add(newFoo)
// 将调用@behavior('add', 'http://localhost:3000/foo', 'POST')定义的行为
const res: Promise<Response[]> = await ctx.saveChanges()
// 检查res
// Update entity
const foo = ctx.foo.find(1)
// 只可以更新非主键成员字段
foo.name = 'Hello'
// 将调用@behavior('update', 'http://localhost:3000/foo', 'POST')定义的行为
ctx.saveChanges()
// Delete entity
const foo1 = ctx.foo.find(1)
const foo2 = ctx.foo.find(2)
ctx.remove(foo1, foo2)
// 将调用@behavior('delete', 'http://localhost:3000/foo', 'POST')定义的行为
ctx.saveChanges()
// CRUD the Included entity
await ctx.foo.include('bar').load(1)
const foo = ctx.foo.find(1)
foo.bar.name = 'World'
// 将调用@behavior('update', 'http://localhost:3000/bar', 'POST')定义的行为
ctx.saveChanges()
await ctx.foo.include('bar').load(1)
const foo = ctx.foo.find(1)
ctx.bar.remove(foo.bar)
// 将调用@behavior('delete', 'http://localhost:3000/bar', 'POST')定义的行为
ctx.saveChanges()
await ctx.foo.include('jar').load(1)
const foo = ctx.foo.find(1)
// 若一个foo关联了二个jar, 那么ctx中将会有三个实体实例被标记为deleted, saveChanges会发出三个请求
ctx.foo.remove(foo)
ctx.saveChange()
const foo1 = new Foo()
const foo2 = new Foo()
// newFoo 赋值
ctx.foo.add(foo1, foo2)
// 将调用@behavior('add', 'http://localhost:3000/foo', 'POST')定义的行为二次
const res: Promise<Response[]> = await ctx.saveChanges()
// res的Promise中将得到一个数组, 即二次请求的结果
// 检查res数据, 以确定每一个请求是否都正确完成
Generated using TypeDoc