N:N(複数対複数)の関連レコード一覧を自作する

目次

はじめに

関連レコード一覧フィールドは関連性が1:Nの場合は標準機能で設定できますが、テーブルの内容を条件とするような、関連性がN:Nの場合は標準機能で設定できません。

こちらの cybozu developer communityでの回答 のように詳細データ設定用のアプリを別途作成して、標準機能で対応する方法もあります。
しかし今回は、JavaScript APIを利用して関連レコード一覧のテーブルを作成・表示するカスタマイズ方法を説明したいと思います。

デモ環境

デモ環境で実際に動作を確認できます。
https://dev-demo.cybozu.com/k/295/ (External link)

ログイン情報は cybozu developer networkデモ環境 で確認してください。

kintoneアプリの作成

サンプルのアプリとして、学習塾クラスの管理アプリを作成します。

生徒管理アプリとクラス管理アプリを作成して、生徒管理アプリから複数のクラスをルックアップ選択し、クラス管理アプリでは、そのクラスに登録した生徒を一覧表示するように設定します。
1人の生徒が複数のクラスを選択でき、ひとつのクラスには複数の生徒が登録されるので、N:Nの関連性となります。

クラス管理アプリの作成

次の画像とテーブルを参考にクラス管理アプリを作成します。

フィールドの種類 フィールド名 フィールドコード 備考
レコード番号 クラス番号 class_no
文字列(1行) クラスコード class_code
文字列(1行) クラス名 class_name
スペース student_list テーブル表示スペース

生徒管理アプリの作成

次の画像とテーブルを参考に生徒管理アプリを作成します。

フィールドの種類 フィールド名 フィールドコード 備考
レコード番号 生徒番号 student_no
文字列(1行) 生徒コード student_code
文字列(1行) 氏名 student_name
ルックアップ クラスコード class_code テーブル
文字列(1行) クラス名 class_name テーブル

サンプルコード

作成した「クラス管理アプリ」に次のソースコードを参考にしたJavaScriptファイルを作成し、適用します。
テーブルのHTMLのクラス名は、kintoneのスタイルに調和するよう、 51-modern-default を参考にしています。
こちらの GitHub上のスタイルシート (External link) をダウンロードし、アプリに反映してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/*
 * N:N(複数対複数)の関連レコード一覧を自作する
 * Copyright (c) 2025 Cybozu
 *
 * Licensed under the MIT License
 */
(() => {
  'use strict';

  // HTMLエスケープ関数
  const escapeHtml = (str) => {
    return str
      .replace(/&/g, '&')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  };

  // 生徒一覧テーブルの作成
  const generateStudentTableHtml = (records, subAppId) => {
    const rows = records
      .map((rec) => {
        const id = escapeHtml(record.$id.value);
        const code = escapeHtml(record.student_code.value);
        const name = escapeHtml(record.student_name.value);
        return `
        <tr>
          <td>
            <div class="kintoneplugin-table-td-control">
              <a href="/k/${subAppId}/show#record=${id}" target="_blank">${code}</a>
            </div>
          </td>
          <td>
            <div class="kintoneplugin-table-td-control">${name}</div>
          </td>
        </tr>`;
      })
      .join('');

    return `
      <table class="kintoneplugin-table">
        <thead>
          <tr>
            <th class="kintoneplugin-table-th" style="width: 250px;"><span class="title">コード</span></th>
            <th class="kintoneplugin-table-th" style="width: 250px;"><span class="title">氏名</span></th>
          </tr>
        </thead>
        <tbody>${rows}</tbody>
      </table>`;
  };

  kintone.events.on(['app.record.detail.show', 'app.record.edit.show'], async (event) => {
    const record = event.record;
    const subAppId = '{生徒管理アプリID}';

    // 増殖バグ回避
    if (document.getElementById('student_list') !== null) {
      return event;
    }

    const subtableSpace = kintone.app.record.getSpaceElement('student_list');
    if (!subtableSpace) {
      console.warn('student_list スペースフィールドが見つかりません');
      return event;
    }

    // 生徒管理アプリからレコード情報を取得
    const params = {
      app: subAppId,
      query: `class_code in ("${record.class_code.value}") order by student_no asc limit 500`,
      fields: ['$id', 'student_code', 'student_name'],
    };

    try {
      const resp = await kintone.api(kintone.api.url('/k/v1/records', true), 'GET', params);
      const tableHtml = generateStudentTableHtml(resp.records, subAppId);
      subtableSpace.innerHTML = tableHtml;
    } catch (error) {
      const errmsg =
        'レコード取得時にエラーが発生しました。' + (error.message ? '\n' + error.message : '');
      subtableSpace.appendChild(document.createTextNode(errmsg));
    }

    return event;
  });
})();

解説

生徒の情報を表示するテーブルを生成しています。

20
21
22
23
24
25
26
27
  // 生徒一覧テーブルの作成
  const generateStudentTableHtml = (records, subAppId) => {
    const rows = records
      .map((record) => {
        const id = escapeHtml(record.$id.value);
        const code = escapeHtml(record.student_code.value);
        const name = escapeHtml(record.student_name.value);
      ・・・
38
39
40
41
42
43
44
      })
      .join('');

    return `
      <table class="kintoneplugin-table">
        <thead>
      ・・・
49
50
51
        <tbody>${rows}</tbody>
      </table>`;
  };

レコード詳細表示およびレコード編集イベントで、生徒一覧テーブル生成処理を実行します。

53
54
  kintone.events.on(['app.record.detail.show', 'app.record.edit.show'], async (event) => {
  ・・・

REST APIを使って、生徒管理アプリからクラス登録した生徒の情報を取得しています。
なお、queryのパラメーターには、クラスコードが一致するレコードのみを取得するように指定します。
取得するフィールドは、「レコード番号」、「生徒コード」、「生徒氏名」となります。

68
69
70
71
72
73
74
75
76
77
78
79
80
    // 生徒管理アプリからレコード情報を取得
    const params = {
      app: subAppId,
      query: `class_code in ("${record.class_code.value}") order by student_no asc limit 500`,
      fields: ['$id', 'student_code', 'student_name'],
    };

    try {
      const resp = await kintone.api(kintone.api.url('/k/v1/records', true), 'GET', params);
      const tableHtml = generateStudentTableHtml(resp.records, subAppId);
      subtableSpace.innerHTML = tableHtml;
    } catch (error) {
   ・・・

なお、クロスサイトスクリプティングの対策として、特殊文字をエスケープ処理しています。

動作確認

生徒管理アプリ、クラス管理アプリそれぞれにいくつかデータを入力します。
生徒管理アプリから、いくつかクラスをルックアップで登録し、「クラスアプリ管理」上に生徒一覧のテーブルが表示されていることを確認します。

まとめ

詳細データ設定用のアプリを別途作成することにより、標準機能で対応も可能です。
しかし、JavaScript APIでカスタマイズすれば、わざわざ詳細データ設定用のアプリを作成しなくて済みます。
コード自体も意外とシンプルですので、ぜひ試してみてください。

information

このTipsは、2025年6月版kintoneで動作を確認しています。