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

## Graph inserts

任意のオブジェクトグラフをデータベースへ挿入する機能。

## 使用法

QueryBuilderのinsertGraphメソッドを使用します。

引数として渡したオブジェクトから、ModelのrelationMappingsの定義に従って、関連するテーブル群に対してレコードを作成してくれます。

注意点としては、insertGraphメソッドはatomicではないため、transactionと併用する必要があります。

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

await transaction(Project.knex(), async (trx) => {
  const project = await Project
    .query(trx)
    .insertGraph({
      name: 'sample',
      members: [
        { name: 'sample_user1', email: 'sample1@example.com' },
        { name: 'sample_user2', email: 'sample2@example.com' }
      ]
    });

  console.log(project);
  // Project {
  //   name: 'sample',
  //   members:
  //    [ User { name: 'sample_user1', email: 'sample1@example.com', id: 7 },
  //      User { name: 'sample_user2', email: 'sample2@example.com', id: 8 } ],
  //   id: 7 }
});
```

PostgreSQLでは、以下のようなSQLが実行されます。

```sql
insert into "projects" ("name") values ('sample') returning "id";

insert into "users" ("email", "name") values ('sample1@example.com', 'sample_user1'), ('sample2@example.com', 'sample_user2') returning "id";

insert into "projectMembers" ("projectId", "userId") values (7, 7), (7, 8) returning "userId";
```

## `"#id"`、`"#ref"`の使用例

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

await transaction(Project.knex(), async (trx) => {
  const projects = await Project
    .query(trx)
    .insertGraph([
      {
        name: 'sample_project1',
        members: [
          {
            '#id': 'sampleUser1',
            name: 'sample_user1',
            email: 'sample1@example.com'
          },
          {
            name: 'sample_user2',
            email: 'sample2@example.com'
          }
        ]
      },
      {
        name: 'sample_project2',
        members: [
          { '#ref': 'sampleUser1' }
        ]
      }
    ]);

  console.log(inspect(projects, { depth: 3 }));
  // [ Project {
  //   name: 'sample_project1',
  //   members:
  //    [ User { name: 'sample_user1', email: 'sample1@example.com', id: 15 },
  //      User { name: 'sample_user2', email: 'sample2@example.com', id: 16 } ],
  //   id: 14 },
  // Project {
  //   name: 'sample_project2',
  //   members:
  //    [ User { name: 'sample_user1', email: 'sample1@example.com', id: 15 } ],
  //   id: 15 } ]

  console.log(projects[0].members[0].id === projects[1].members[0].id); // true
});
```

以下のようなSQLが実行されます。

```sql
insert into "projects" ("name") values ('sample_project1'), ('sample_project2') returning "id";

insert into "users" ("email", "name") values ('sample1@example.com', 'sample_user1'), ('sample2@example.com', 'sample_user2') returning "id";

insert into "projectMembers" ("projectId", "userId") values (16, 17), (16, 18), (17, 17) returning "userId";
```

* 複数のプロジェクトを同時に作成したい。
* その際に、プロジェクトに所属するユーザも一緒に作成したい。
* あるユーザを複数のプロジェクトに所属させたい。

というようなケースを想定します。

その際は、上記のように`"#id"`と`"#ref"`を使用することで実現できます。

`#id`プロパティで、グラフ内のあるモデルに対する参照を作成し、

`#ref`プロパティで、グラフ内の別の場所からそのモデルを参照することができます。

## relateオプション

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

await transaction(Project.knex(), async (trx) => {
  const user = await User
    .query()
    .where({ email: 'taro@example.com' })
    .first();

  const project = await Project
    .query(trx)
    .insertGraph({
      name: 'relate_sample',
      members: [
        { id: user.id }
      ]
    }, {
      relate: true
    });

  console.log(project);
  // Project { name: 'relate_sample', members: [ User { id: 1 } ], id: 24 }
});
```

以下のようなSQLが実行されます。

```sql
insert into "projects" ("name") values ('relate_sample') returning "id";

insert into "projectMembers" ("projectId", "userId") values (24, 1) returning "userId";
```

* プロジェクトを作成する
* データベース上にすでに存在するユーザをそのプロジェクトに所属させる

というようなケースを想定しています。

このようなケースは、insertGraphメソッドの第2引数に、relateプロパティをtrueに設定したオブジェクトを渡すことで実現できます。

relateプロパティをtrueに指定することで、すでにデータベース上に存在するレコードを、新規に作成されるレコードと関連づけることができます。

## より複雑な例

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

await transaction(Project.knex(), async (trx) => {
  const user = await User
    .query()
    .where({ email: 'taro@example.com' })
    .first();

  const projects = await Project
    .query(trx)
    .insertGraph([
      {
        '#id': 'project1',
        name: 'sample1',
        members: [
          {
            '#id': 'user1',
            name: 'user1',
            email: 'user1@example.com',
            tasks: [
              {
                name: 'task1',
                projectId: '#ref{project1.id}'
              }
            ]
          },
          { id: user.id }
        ]
      },
      {
        '#id': 'project2',
        name: 'sample2',
        members: [
          {
            '#ref': 'user1',
            tasks: [
              {
                name: "#ref{user1.name}'s task",
                projectId: '#ref{project2.id}'
              }
            ]
          }
        ]
      }
    ], { relate: true });

  console.log(inspect(projects, { depth: 5 }));
  // [ Project {
  //     name: 'sample1',
  //     members:
  //      [ User {
  //          name: 'user1',
  //          email: 'user1@example.com',
  //          tasks:
  //           [ Task { name: 'task1', projectId: 33, assignedTo: 23, id: 7 } ],
  //          id: 23 },
  //        User { id: 1 } ],
  //     id: 33 },
  //   Project {
  //     name: 'sample2',
  //     members:
  //      [ User {
  //          tasks:
  //           [ Task { name: 'user1\'s task', projectId: 34, assignedTo: 23, id: 8 } ],
  //          name: 'user1',
  //          email: 'user1@example.com',
  //          id: 23 } ],
  //     id: 34 } ]
});
```

以下のようなSQLが実行されます。

```sql
insert into "projects" ("name") values ('sample1'), ('sample2') returning "id";

insert into "users" ("email", "name") values ('user1@example.com', 'user1') returning "id";

insert into "tasks" ("assignedTo", "name", "projectId") values (23, 'task1', 33), (23, 'user1\'s task', 34) returning "id";

insert into "projectMembers" ("projectId", "userId") values (33, 23), (33, 1), (34, 23) returning "userId";
```

上記のように、3階層以上のModelにまたがる処理も実行可能。

`#ref{<id>.<property>}`の形式の値を使用することで、あるModelのプロパティを設定する際に、別のモデルのプロパティの値を参照できます。

## 参考

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