レコードIDを使った一括取得

にメンテナンス済み

kintone では REST API が提供されており、保存されたレコード情報を他のアプリ、または外部サービスから取得することができます。

しかし、一度に取得できるレコード数には制限があり、その制限を超える場合は複数回に分けて取得する必要があります。

複数回に分けて取得するアプローチとして、以下の方法があります。

  • カーソルを使った取得
  • レコード ID を使った取得

このうち、カーソル API は夜間バッチ処理などで使用することを想定しており、同一ドメイン内で同時に 10 個しか作成できないという制約があります。

そのため、通常のアプリケーションで使用する場合は、レコード ID を使った取得方法が適していると言えます。

このページでは、シーク法と呼ばれるレコード ID をもとに一括取得する方法を紹介します。

取得方法について

今回実施する取得方法の流れは、大きく分けて以下の通りです。

  • レコード ID で並び替える
  • レコードを取得する
  • レコードの取得数が取得上限と同様の場合は、最後に取得したレコード ID より小さい(もしくは大きい)レコードを取得する
  • 取得数が上限でない場合は、全レコードを取得したものとして返却

要は、レコード ID 順に少しずつレコードを集めていく方法です。

非常にシンプルで、カーソル API を使用する方法より理解しやすいんじゃないかと思います。

developer network に掲載されているコードがよくわからない

まず、developer network に掲載されているレコード ID を使用した方法を確認したところ、やりたいことは分かりましたが、サンプルコードがよくわかりませんでした。

矢印をクリックするとソースコード全体を確認できます

developer network に掲載されているコード
/*
 * get all records function by using record id sample program
 * Copyright (c) 2019 Cybozu
 *
 * Licensed under the MIT License
 */

/*
 * @param {Object} params
 *   - app {String}: アプリID(省略時は表示中アプリ)
 *   - filterCond {String}: 絞り込み条件
 *   - sortConds {Array}: ソート条件の配列
 *   - fields {Array}: 取得対象フィールドの配列
 * @return {Object} response
 *   - records {Array}: 取得レコードの配列
 */
const getRecords = (_params) => {
  const MAX_READ_LIMIT = 500;

  const params = _params || {};
  const app = params.app || kintone.app.getId();
  const filterCond = params.filterCond;
  const sortConds = params.sortConds || ['$id asc'];
  const fields = params.fields;
  let data = params.data;

  if (!data) {
    data = {
      records: [],
      lastRecordId: 0,
    };
  }

  const conditions = [];
  const limit = MAX_READ_LIMIT;
  if (filterCond) {
    conditions.push(filterCond);
  }

  conditions.push('$id > ' + data.lastRecordId);

  const sortCondsAndLimit = ` order by ${sortConds.join(', ')} limit ${limit}`;
  const query = conditions.join(' and ') + sortCondsAndLimit;
  const body = {
    app: app,
    query: query,
  };

  if (fields && fields.length > 0) {
    // $id で並び替えを行うため、取得フィールドに「$id」フィールドが含まれていなければ追加します
    if (fields.indexOf('$id') <= -1) {
      fields.push('$id');
    }
    body.fields = fields;
  }

  return kintone.api(kintone.api.url('/k/v1/records', true), 'GET', body).then((r) => {
    data.records = data.records.concat(r.records);
    if (r.records.length === limit) {
      // 取得レコードの件数が limit と同じ場合は、未取得のレコードが残っている場合があるので、getRecords を再帰呼び出して、残りのレコードを取得します
      data.lastRecordId = r.records[r.records.length - 1].$id.value;
      return getRecords({
        app: app,
        filterCond: filterCond,
        sortConds: sortConds,
        fields: fields,
        data: data,
      });
    }
    delete data.lastRecordId;
    return data;
  });
};

今回の取得方法が適しているシナリオとして、以下のように紹介されています。

この方法は、次のようなシナリオに適しています。

レコードのソート条件を必要としない場合(レコード ID 順で問題ない場合) レコード ID 順にレコードを取得した後、プログラムのロジックで別のソートができる場合 offset の制限値を考慮した kintone のレコード一括取得について

ただ、サンプルコードには引数にソート順を指定することができてしまいますし、ソート順を指定すると正しく動作しません。

公式のライブラリを確認する

次に、公式に提供されている SDKを確認しました。

レコードの一括取得を行っている部分はここです。

SDKのソースコード
class RecordClient {
  /* ..省略.. */

  public async getAllRecordsWithId<T extends Record>(params: {
    app: AppID;
    fields?: string[];
    condition?: string;
  }): Promise<T[]> {
    const { fields: originalFields, ...rest } = params;
    let fields = originalFields;
    // Append $id if $id doesn't exist in fields
    if (fields && fields.length > 0 && fields.indexOf('$id') === -1) {
      fields = [...fields, '$id'];
    }
    return this.getAllRecordsRecursiveWithId({ ...rest, fields }, '0', []);
  }

  private async getAllRecordsRecursiveWithId<T extends Record>(
    params: {
      app: AppID;
      fields?: string[];
      condition?: string;
    },
    id: string,
    records: T[]
  ): Promise<T[]> {
    const GET_RECORDS_LIMIT = 500;

    const { condition, ...rest } = params;
    const conditionQuery = condition ? `(${condition}) and ` : '';
    const query = `${conditionQuery}$id > ${id} order by $id asc limit ${GET_RECORDS_LIMIT}`;
    const result = await this.getRecords<T>({ ...rest, query });
    const allRecords = records.concat(result.records);
    if (result.records.length < GET_RECORDS_LIMIT) {
      return allRecords;
    }
    const lastRecord = result.records[result.records.length - 1];
    if (lastRecord.$id.type === '__ID__') {
      const lastId = lastRecord.$id.value;
      return this.getAllRecordsRecursiveWithId(params, lastId, allRecords);
    }
    throw new Error(
      'Missing `$id` in `getRecords` response. This error is likely caused by a bug in Kintone REST API Client. Please file an issue.'
    );
  }

  /* ..省略.. */
}

getAllRecordsWithId部分でデータを整えた後、getAllRecordsRecursiveWithIdで再帰的にレコードの一括取得を実施しています。今度はソート条件を指定できません。

@kintone/rest-api-client を使用できる環境であれば、getAllRecordsWithRecordIdを使用すればほとんどのシナリオで解決できそうです。

意味自体は理解できたので、1 から関数を作成していきます。

サンプルコード

ここまでの情報をふまえて、レコード ID を使ったレコードの一括取得関数を自作しました。

TypeScript

const CHUNK_SIZE = 500;

export const getAllRecordsWithId = async <
  T extends { $id: unknown } & Record<string, unknown>
>(props: {
  app: number | string;
  fields?: (keyof T)[];
  onStep?: (current: T[]) => void;
  condition?: string;
}): Promise<T[]> => {
  const { fields: originalFields = [], condition = '' } = props;

  const fields = [...new Set([...originalFields, '$id'])];

  return getRecursive<T>({ ...props, fields, condition });
};

const getRecursive = async <T>(props: {
  app: number | string;
  fields: (keyof T)[];
  condition: string;
  onStep?: (current: T[]) => void;
  id?: string;
  stored?: T[];
}): Promise<T[]> => {
  const { app, fields, condition, id } = props;

  const newCondition = id ? `${condition ? `${condition} and ` : ''} $id < ${id}` : condition;

  const query = `${newCondition} order by $id desc limit ${CHUNK_SIZE}`;

  const { records } = await kintone.api(kintone.api.url('/k/v1/records', true), 'GET', {
    app,
    fields,
    query,
  });

  const stored = [...(props.stored ?? []), ...records];

  if (props.onStep) {
    props.onStep(stored);
  }

  const lastRecord = stored[stored.length - 1];
  const lastId = lastRecord.$id.value;

  return records.length === CHUNK_SIZE ? getRecursive({ ...props, id: lastId, stored }) : stored;
};

JavaScript

const CHUNK_SIZE = 500;

/**
 * @template T
 * @param props {{ app: number | string; fields?: (keyof T)[]; onStep?: (current: T[]) => void; condition?: string; }}
 * @returns { Promise<T[]> }
 */
export const getAllRecordsWithId = async (props) => {
  const { fields: originalFields = [], condition = '' } = props;

  const fields = [...new Set([...originalFields, '$id'])];

  return getRecursive({ ...props, fields, condition });
};

/**
 * @template T
 * @param props {{ app: number | string; fields: (keyof T)[]; condition: string; onStep?: (current: T[]) => void; id?: string; stored?: T[];}}
 * @returns  { Promise<T[]> }
 */
const getRecursive = async (props) => {
  const { app, fields, condition, id } = props;

  const newCondition = id ? `${condition ? `${condition} and ` : ''} $id < ${id}` : condition;

  const query = `${newCondition} order by $id desc limit ${CHUNK_SIZE}`;

  const { records } = await kintone.api(kintone.api.url('/k/v1/records', true), 'GET', {
    app,
    fields,
    query,
  });

  const stored = [...(props.stored ?? []), ...records];

  if (props.onStep) {
    props.onStep(stored);
  }

  const lastRecord = stored[stored.length - 1];
  const lastId = lastRecord.$id.value;

  return records.length === CHUNK_SIZE ? getRecursive({ ...props, id: lastId, stored }) : stored;
};

ポイント

SDK に倣って関数を 2 つに分けましたが、これはどちらでもよいと思います。

引数

引数についてですが、ポータビリティを担保するためにアプリ ID を必須にしています。

未指定の場合はkintone.getId()を実行するように設定することも可能ですが、予期せぬエラーにつながることもあるため今回は必須としました。

また、今回やりたかったのはonStepというコールバック関数を引数に加えることです。

これによって、全てのデータを取得しきる前にも、随時最新のレコード情報について処理を実行したい場合にも対応できるようになります。

取得順

サンプルではレコード ID の昇順にデータを取得していましたが、古いデータより新しいデータの方がより早く知りたいケースが多いんじゃないかと思ったので、レコード ID の降順で取得しています。

シーク法の記事を読む限り、昇順でも問題ないようですが、降順の方が使いやすいのではないかと考えました。

まとめ

今回は、レコード ID を使ってアプリのレコードを一括で取得する方法を紹介しました。

シンプルな方法で、カーソル API を使用する方法よりも理解しやすいと思います。

また、公式の SDK にはレコード ID を使った取得方法が提供されているため、それを参考にして関数を作成しました。

この関数を使うことで、レコード ID を使ってアプリのレコードを一括で取得することができます。

ぜひ、お試しください。

#kintone #JavaScript #TypeScript