ORM
Mirage 最初只包含一个数据库作为其数据层。虽然有用,但用户仍然需要编写大量代码来模拟现代复杂的后台系统。尤其是处理关系是一个很大的痛点。
解决方案是为 Mirage 添加一个对象关系映射器 (ORM)。
让我们看看 ORM 如何帮助 Mirage 为您承担更多繁重工作。
为什么使用 ORM?
考虑一个像这样的数据库:
db.dump()
// Result
{
movies: [
{ id: "1", title: "Interstellar" },
{ id: "2", title: "Inception" },
{ id: "3", title: "Dunkirk" },
]
}
在编写路由处理器时,您遇到的第一个问题是如何将这些原始数据转换为您的应用期望的格式——即,如何匹配生产 API 的格式。
假设您的后台使用JSON:API 规范。您对 /api/movies/1
的 GET 请求的响应应如下所示:
// GET /api/movies/1
{
"data": {
"id": "1",
"type": "movies",
"attributes": {
"title": "Interstellar"
}
}
}
没什么大不了的——我们只需在路由处理器中直接编写此格式化逻辑即可:
this.get("/movies/:id", (schema, request) => {
let movie = schema.db.movies.find(request.params.id)
return {
data: {
id: movie.id,
type: "movies",
attributes: {
title: movie.title,
},
},
}
})
这可以正常工作。但假设我们的 Movie
模型有几个额外的属性:
{
"id": "1",
"title": "Interstellar",
"releaseDate": "October 26, 2014",
"genre": "Sci-Fi"
}
现在,我们的路由处理器需要更加智能,并确保除 id
之外的所有属性都最终出现在 attributes
哈希中:
this.get('/movies/:id', (schema, request) => {
let movie = schema.db.movies.find(request.params.id);
let movieJSON = {
data: {
id: movie.id,
type: 'movies',
attributes: { }
}
};
Object.keys(movie)
.filter(key => key !=== 'id')
.forEach(key => {
movieJSON.attributes[key] = movie[key];
});
return movieJSON;
});
如您所见,事情很快就会变得复杂。
如果我们在其中添加关系呢?假设一个 Movie
与一个 director
有关系,并且它使用 directorId
外键存储该关系:
attrs = {
id: "1",
title: "Interstellar",
releaseDate: "October 26, 2014",
genre: "Sci-Fi",
directorId: "23",
}
此模型的预期 HTTP 响应现在如下所示:
{
"data": {
"id": "1",
"type": "movies",
"attributes": {
"title": "Interstellar"
},
"relationships": {
"directors": {
"data": { "type": "people", "id": "23" }
}
}
}
}
这意味着我们的路由处理器需要变得更加复杂。特别是,它们需要一种可靠的方法来区分模型的属性(如 title
)及其关系键(如 directorId
)。
事实证明,这些类型的问题非常普遍,只要 Mirage 了解您的应用模型及其关系,我们就可以通用地解决它们。
ORM 解决的问题
当 Mirage 了解您的应用域时,它可以承担正确实现模拟服务器所需的低级簿记工作的责任。
让我们看看它如何做到这一点的一些示例。
格式化逻辑分离
首先,我们可以通过定义 Mirage 模型来告诉 Mirage 我们的应用模式。这些模型在 ORM 中注册,并告诉 Mirage 您的数据结构。
让我们定义一个 Movie
模型。
import { createServer, Model } from "miragejs"
createServer({
models: {
movie: Model.extend(),
},
})
Mirage 模型在属性方面是无模式的,这意味着它不需要你定义像 title
和 releaseDate
这样的普通属性。因此,无论你的 Movie
模型具有哪些属性,上面的模型定义都有效。
定义了 Movie
模型后,我们可以更新我们的路由处理程序以使用 ORM 响应 Mirage 模型实例。
import { createServer, Model } from "miragejs"
createServer({
models: {
movie: Model.extend(),
},
routes() {
this.get("/movies/:id", (schema, request) => {
let id = request.params.id
return schema.movies.find(id)
})
},
})
schema
参数是与 ORM 交互的方式。
从路由处理程序返回 Mirage 模型的实例,而不是普通 JavaScript 对象,我们现在可以利用 Mirage 的序列化层。序列化程序的工作原理是将模型和集合转换为格式化的 JSON 响应。
Mirage 默认附带一个 JSONAPISerializer,所以如果我们将其设置为应用程序序列化程序
import { createServer, Model, JSONAPISerializer } from "miragejs"
createServer({
models: {
movie: Model.extend(),
},
serializers: {
application: JSONAPISerializer,
},
routes() {
this.get("/movies/:id", (schema, request) => {
let id = request.params.id
return schema.movies.find(id)
})
},
})
这个路由处理程序现在将响应我们期望的有效负载。
/* GET /movies/1 */
{
"data": {
"id": "1",
"type": "movies",
"attributes": {
"title": "Interstellar",
"releaseDate": "October 26, 2014",
"genre": "Sci-Fi"
}
}
}
ORM 已经通过将模型转换为 JSON 的工作委托给序列化层来帮助我们保持路由处理程序的整洁。
但当我们将关系添加到混合中时,它会变得更加强大。
获取相关数据
假设我们的 Movie
与 director
之间存在一对多关系。
// mirage/models/movie.js
import { createServer, Model, belongsTo, JSONAPISerializer } from "miragejs"
createServer({
models: {
person: Model.extend(),
movie: Model.extend({
director: belongsTo("person"),
}),
},
serializers: {
application: JSONAPISerializer,
},
routes() {
this.get("/movies/:id", (schema, request) => {
let id = request.params.id
return schema.movies.find(id)
})
},
})
director
是一个指向 Person
模型的命名关系。
无需更改路由处理程序或序列化程序中的任何内容,我们现在可以使用 JSON:API includes 获取数据图。
以下请求
GET /api/movies/1?include=director
现在生成此响应
{
"data": {
"id": "1",
"type": "movies",
"attributes": {
"title": "Interstellar",
"releaseDate": "October 26, 2014",
"genre": "Sci-Fi"
},
"relationships": {
"director": {
"data": { "type": "people", "id": "1" }
}
}
},
"included": [
{
"id": "1",
"type": "people",
"attributes": {
"name": "Christopher Nolan"
}
}
]
}
JSONAPISerializer 能够检查 ORM,以便它能够将所有模型、属性和关系放在正确的位置。我们的路由处理程序根本不需要更改。
事实上,我们编写的路由处理程序与 Shorthand 等效项的默认行为相同,这意味着我们可以切换使用它。
- this.get('/movies/:id', (schema, request) => {
- let id = request.params.id;
- return schema.movies.find(id);
- });
+ this.get('/movies/:id');
这是 ORM 如何帮助 Mirage 的各个部分(例如 Shorthands 和 Serializers)协同工作以简化服务器定义的另一个示例。
创建和编辑相关数据
ORM 还使创建和编辑相关数据比仅使用原始数据库记录更容易。
例如,要使用仅数据库创建 Movie
和 Person
以及它们之间的关系,你需要做类似这样的事情
server.db.loadData({
people: [
{
id: "1",
name: "Christopher Nolan",
},
],
movies: [
{
id: "1",
title: "Interstellar",
releaseDate: "October 26, 2014",
genre: "Sci-Fi",
directorId: "1",
},
],
})
请注意,Movies
记录上的 directorId
外键必须与关联 People
记录上的 id
相匹配。
像这样管理原始数据库数据很快就会变得难以处理,尤其是在关系随着时间推移而发生变化时。
通过 server.schema
使用 ORM,我们可以创建这个图而无需管理任何 ID。
let nolan = schema.people.create({ name: "Christopher Nolan" })
schema.movies.create({
director: nolan,
title: "Interstellar",
releaseDate: "October 26, 2014",
genre: "Sci-Fi",
})
在创建电影时将模型实例 nolan
作为 director
属性传递就足以正确设置所有键。
ORM 还可以在编辑关系时保持外键同步。鉴于数据库
{
movies: [
{
id: '1',
title: 'Star Wars: The Rise of Skywalker',
directorId: '2'
}
],
people: [
{
id: '2',
name: 'Rian Johnson'
},
{
id: '3',
name: 'J.J. Abrams'
}
]
}
我们可以像这样更新电影的导演
let episode9 = schema.movies.findBy({
title: 'Star Wars: The Rise of Skywalker'
});
episode9.update({
director: schema.people.findBy({ name: 'J.J. Abrams' });
});
新数据库将如下所示
{
movies: [
{
id: '1',
title: 'Star Wars: The Rise of Skywalker',
directorId: '3'
}
],
people: [
{
id: '2',
name: 'Rian Johnson'
},
{
id: '3',
name: 'J.J. Abrams'
}
]
}
请注意,directorId
在数据库中已更改,即使我们只使用过模型实例。
重要的是,对于更复杂的关系(如具有逆关系的一对多关系或多对多关系)也是如此。
ORM 使 Mirage 能够将所有这些簿记工作从你的代码中抽象出来,甚至为 Shorthands 提供了足够的权力来尊重对复杂关系图的任意更新。
这些是 Mirage 的 ORM 解决的一些主要问题。通常,当 Mirage 了解你的应用程序的模式时,它可以承担更多配置模拟服务器的责任。
接下来,我们将看看如何在 Mirage 中实际定义你的模型及其关系。