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

如您所見,事情很快就變得複雜起來。

如果我們將關聯關係加入其中呢?假設 Moviedirector 之間存在關聯關係,並且它使用 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 模型在屬性上是無綱要的,也就是說它們不需要你定義像 titlereleaseDate 這樣的純屬性。因此,無論你的 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)
    })
  },
})

這個路由處理器現在將會回傳我們預期的 payload。

/* GET /movies/1 */

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Interstellar",
      "releaseDate": "October 26, 2014",
      "genre": "Sci-Fi"
    }
  }
}

ORM 已經幫助我們保持路由處理器的整潔,將模型轉換為 JSON 的工作委派給序列化器層。

當我們加入關聯時,它會變得更加強大。

假設我們的 Movie 與一個 director 有一個 belongs-to 的關聯。

// 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 和序列化器)協同工作以簡化伺服器定義的另一個例子。

如果僅使用原始資料庫記錄,ORM 也使得建立和編輯相關資料更容易。

例如,要僅使用資料庫建立具有關聯的 MoviePerson,你需要執行類似這樣的操作

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 中實際定義你的模型及其關聯。