Node.js向けORM Objection.jsを使ってみた(6) - Graph upserts

## Graph upserts

オブジェクトグラフを元に、データベース上のレコードの登録・更新・削除を行う機能。

## 使用例

以下のようなデータがデータベースに登録されているとします。

```javascript
[ Project {
    id: 53,
    name: 'project1',
    ownerId: null,
    members:
     [ User {
         id: 48,
         name: 'hoge',
         email: 'hoge@example.com',
         tasks:
          [ Task {
              id: 15,
              name: 'hoge\'s task1',
              description: null,
              assignedTo: 48,
              projectId: 53,
              deadline: null,
              createdAt: 2018-08-19T13:03:57.761Z,
              updatedAt: 2018-08-19T13:03:57.761Z },
            Task {
              id: 16,
              name: 'hoge\'s task2',
              description: null,
              assignedTo: 48,
              projectId: 53,
              deadline: null,
              createdAt: 2018-08-19T13:03:57.761Z,
              updatedAt: 2018-08-19T13:03:57.761Z } ] },
       User { id: 49, name: 'piyo', email: 'piyo@example.com', tasks: [] } ] },
  Project {
    id: 54,
    name: 'project2',
    ownerId: null,
    members:
     [ User {
         id: 48,
         name: 'hoge',
         email: 'hoge@example.com',
         tasks:
          [ Task {
              id: 15,
              name: 'hoge\'s task1',
              description: null,
              assignedTo: 48,
              projectId: 53,
              deadline: null,
              createdAt: 2018-08-19T13:03:57.761Z,
              updatedAt: 2018-08-19T13:03:57.761Z },
            Task {
              id: 16,
              name: 'hoge\'s task2',
              description: null,
              assignedTo: 48,
              projectId: 53,
              deadline: null,
              createdAt: 2018-08-19T13:03:57.761Z,
              updatedAt: 2018-08-19T13:03:57.761Z } ] },
       User {
         id: 50,
         name: 'fuga',
         email: 'fuga@example.com',
         tasks:
          [ Task {
              id: 17,
              name: 'fuga\'s task1',
              description: null,
              assignedTo: 50,
              projectId: 54,
              deadline: 2018-09-30T15:00:00.000Z,
              createdAt: 2018-08-19T13:03:57.761Z,
              updatedAt: 2018-08-19T13:03:57.761Z } ] } ] } ]
```

```javascript
const { transaction } = require('objection');
const { inspect } = require('util');

await transaction(Project.knex(), async (trx) => {
  const projects = await Project.query(trx)
    .upsertGraph([
      {
        id: 53,
        members: [
          {
            id: 48,
            name: 'hoge',
            tasks: [
              // idが15のtaskを更新
              {
                id: 15,
                name: 'hoge\'s task1',
                description: 'hogeのタスク',
                assignedTo: 48
              }
              // idが16のtaskは指定されていないため、データベース上から関連が削除されます。
            ]
          },
          {
            // idを持っていないため、データベースに新しいレコードが作成される。
            '#id': 'hogepiyo',
            name: 'hogepiyo',
            email: 'hogepiyo@example.com',
            tasks: [
              {
                // idを持っていないため、データベースに新しいレコードが作成される。
                name: "#ref{hogepiyo.name}'s task",
                projectId: 53
              }
            ]
          }
        ]
      },
      {
        // idを持っていないため、データベースに新しいレコードが作成される。
        name: 'project3',
        members: [
          { id: 48 },
          { '#ref': 'hogepiyo' }
        ]
      }
    ], {
      relate: ['members', 'members.tasks'],
      unrelate: ['members'] 
    });

  console.log(inspect(projects, { depth: 5 }));

  // [ Project {
  //     id: 53,
  //     members:
  //      [ User {
  //          id: 48,
  //          name: 'hoge',
  //          tasks:
  //           [ Task {
  //               id: 15,
  //               name: 'hoge\'s task1',
  //               description: 'hogeのタスク',
  //               assignedTo: 48 } ] },
  //        User {
  //          name: 'hogepiyo',
  //          email: 'hogepiyo@example.com',
  //          tasks:
  //           [ Task {
  //               name: 'hogepiyo\'s task',
  //               projectId: 53,
  //               assignedTo: 54,
  //               id: 20 } ],
  //          id: 54 } ] },
  //   Project {
  //     name: 'project3',
  //     members:
  //      [ User { id: 48 },
  //        User { name: 'hogepiyo', email: 'hogepiyo@example.com', id: 54 } ],
  //     id: 58 } ]
});
```

上記を実行すると、データベース上のデータは、以下のような状態になります。

```sql
[ Project {
    id: 53,
    name: 'project1',
    ownerId: null,
    members:
     [ User {
         id: 48,
         name: 'hoge',
         email: 'hoge@example.com',
         tasks:
          [ Task {
              id: 15,
              name: 'hoge\'s task1',
              description: 'hogeのタスク',
              assignedTo: 48,
              projectId: 53,
              deadline: null,
              createdAt: 2018-08-19T13:03:57.761Z,
              updatedAt: 2018-08-19T13:03:57.761Z } ] },
       User {
         id: 54,
         name: 'hogepiyo',
         email: 'hogepiyo@example.com',
         tasks:
          [ Task {
              id: 20,
              name: 'hogepiyo\'s task',
              description: null,
              assignedTo: 54,
              projectId: 53,
              deadline: null,
              createdAt: 2018-08-19T13:12:08.443Z,
              updatedAt: 2018-08-19T13:12:08.443Z } ] } ] },
  Project {
    id: 54,
    name: 'project2',
    ownerId: null,
    members:
     [ User {
         id: 48,
         name: 'hoge',
         email: 'hoge@example.com',
         tasks:
          [ Task {
              id: 15,
              name: 'hoge\'s task1',
              description: 'hogeのタスク',
              assignedTo: 48,
              projectId: 53,
              deadline: null,
              createdAt: 2018-08-19T13:03:57.761Z,
              updatedAt: 2018-08-19T13:03:57.761Z } ] },
       User {
         id: 50,
         name: 'fuga',
         email: 'fuga@example.com',
         tasks:
          [ Task {
              id: 17,
              name: 'fuga\'s task1',
              description: null,
              assignedTo: 50,
              projectId: 54,
              deadline: 2018-09-30T15:00:00.000Z,
              createdAt: 2018-08-19T13:03:57.761Z,
              updatedAt: 2018-08-19T13:03:57.761Z } ] } ] },
  Project {
    id: 58,
    name: 'project3',
    ownerId: null,
    members:
     [ User {
         id: 48,
         name: 'hoge',
         email: 'hoge@example.com',
         tasks:
          [ Task {
              id: 15,
              name: 'hoge\'s task1',
              description: 'hogeのタスク',
              assignedTo: 48,
              projectId: 53,
              deadline: null,
              createdAt: 2018-08-19T13:03:57.761Z,
              updatedAt: 2018-08-19T13:03:57.761Z } ] },
       User {
         id: 54,
         name: 'hogepiyo',
         email: 'hogepiyo@example.com',
         tasks:
          [ Task {
              id: 20,
              name: 'hogepiyo\'s task',
              description: null,
              assignedTo: 54,
              projectId: 53,
              deadline: null,
              createdAt: 2018-08-19T13:12:08.443Z,
              updatedAt: 2018-08-19T13:12:08.443Z } ] } ] } ]
```
upsertGraphメソッドはデフォルトで、以下のように動作します。

* グラフ中のidを持たないオブジェクトは、データベースの対応するテーブルに新しいレコードとして登録される。
* グラフ中のidを持つオブジェクトは、データベースの対応するレコードが更新される。
* データベース上には関連が存在するが、グラフ中に現れなかったオブジェクトは、データベースからレコードが削除される。

内部的にinsertGraphメソッドを利用しているため、`#id`や`#ref`の機能を利用することができます。

上記の例では、第2引数でrelateおよびunrelateオプションを指定することで、デフォルトの挙動を変更しています。

```javascript
{
  // membersおよびtasksについては、データベース上に存在するレコードに対する関連を作成できるようにする
  relate: ['members', 'members.tasks'],

  // membersについては、グラフ中に登場しないオブジェクトがあった際は、データベース上のレコードは削除せず、関連の削除のみを行う(usersテーブルからはレコードを削除せず、joinテーブルのみからレコードを削除する)
  unrelate: ['members'] 
}
```

insertGraph同様、デフォルトではatomicではないので、transactionと併用する必要があります。

## 参考

https://vincit.github.io/objection.js/#graph-upserts