DocumentClientに拡張Arrayを渡したらMapにされる件を調査してみた

記事タイトルとURLをコピーする

こんにちは、PE部の中村です。
今日は、とあるシステム(typescript製)の開発中にAWS SDKのある挙動にハマってしまったので、その話を備忘録として残しておきます。

起きたこと

aws-sdkにおいて、DynamoDBのDocumentClientにArrayを継承した独自クラスを渡すと、 ['hoge', 'fuga', 'piyo'] のようなList型となって欲しいところが { '0': 'hoge', '1': 'fuga', '2': 'piyo' } のようなMap型となってしまう。

検証用リポジトリ

再現の準備

まず、下記のようにArrayを継承したExtendedArrayというクラスをGenericsを使って作成します。そして、Personクラスを作成し、それをまとめるPeopleクラスを、先程のExtendedArrayから継承して作成します。

models.ts

class ExtendedArray<T> extends Array<T> {
  print(): void {
    console.log(this.join(', '));

    return;
  }

  toRawObject(): T[] {
    return this.map(item => item);
  }

  toRawObject2(): T[] {
    return new Array(...this);
  }

  toRawObject3(): T[] {
    return JSON.parse(JSON.stringify(this));
  }
}

export class Person {
  firstName = '';
  lastName = '';
  age = 0;

  constructor(init: Person) {
    Object.assign(this, init);
  }
}

export class People extends ExtendedArray<Person> {}

そして、実際にDynamoDBにデータを渡していきます。今回は5パターン用意してみました。
5パターンの説明に入る前に一旦ソースコードです。

insertdata.ts

const main = async (): Promise<void> => {
  const people = new People(
    new Person({ age: 5, firstName: 'momotaro', lastName: 'yamada' }),
    new Person({ age: 6, firstName: 'takashi', lastName: 'suzuki' }),
    new Person({ age: 7, firstName: 'koichi', lastName: 'sato' })
  );
  const rawPeople = [
    new Person({ age: 5, firstName: 'momotaro', lastName: 'yamada' }),
    new Person({ age: 6, firstName: 'takashi', lastName: 'suzuki' }),
    new Person({ age: 7, firstName: 'koichi', lastName: 'sato' })
  ];

  const testdata: Testcase[] = [
    {
      message: 'extended array',
      data: people
    },
    {
      message: 'raw array',
      data: rawPeople
    },
    {
      message: 'converted to raw array from extended array using Array.map()',
      data: people.toRawObject()
    },
    {
      message: 'converted to raw array from extended array using new Array(...this)',
      data: people.toRawObject2()
    },
    {
      message: 'converted to raw array from extended array using JSON.parse(JSON.stringify(this))',
      data: people.toRawObject3()
    }
  ];

  for (const td of testdata) {
    console.log(`Array.isArray(): ${Array.isArray(td.data)}`);
    console.log(td.data);
    await putAndView(td);
  }
};
  • パターン1: Arrayを拡張したPeopleクラスをそのまま渡す
  • パターン2: Peopleクラスは使わずに、Personクラスのインスタンスを素のArrayに入れて渡す
  • パターン3: PeopleクラスにPerson[] を返す関数 this.map(item => item) を実装し、その関数の返り値を渡す
  • パターン4: PeopleクラスにPerson[] を返す関数 new Array(...this) を実装し、その関数の返り値を渡す
  • パターン5: Peopleクラスのインスタンスを 文字列化 -> オブジェクト化する関数 JSON.parse(JSON.stringify(this)) を実装し、その関数の返り値を渡す

結果

結果は以下のようになりました。

  • パターン1: Mapになってしまう
  • パターン2: Listのまま
  • パターン3: Mapになってしまう(!?)
  • パターン4: Listのまま
  • パターン5: Listのまま

README.md

npm run insert

> ddb-documentclient-test@0.1.0 insert /Users/xxx/ddb-documentclient-test
> npx ts-node src/insertdata.ts

Array.isArray(): true
People [
  Person { firstName: 'momotaro', lastName: 'yamada', age: 5 },
  Person { firstName: 'takashi', lastName: 'suzuki', age: 6 },
  Person { firstName: 'koichi', lastName: 'sato', age: 7 }
]
{
  "id": "extended array",
  "data": {
    "0": {
      "firstName": "momotaro",
      "lastName": "yamada",
      "age": 5
    },
    "1": {
      "firstName": "takashi",
      "lastName": "suzuki",
      "age": 6
    },
    "2": {
      "firstName": "koichi",
      "lastName": "sato",
      "age": 7
    }
  }
}
Array.isArray(): true
[
  Person { firstName: 'momotaro', lastName: 'yamada', age: 5 },
  Person { firstName: 'takashi', lastName: 'suzuki', age: 6 },
  Person { firstName: 'koichi', lastName: 'sato', age: 7 }
]
{
  "id": "raw array",
  "data": [
    {
      "firstName": "momotaro",
      "lastName": "yamada",
      "age": 5
    },
    {
      "firstName": "takashi",
      "lastName": "suzuki",
      "age": 6
    },
    {
      "firstName": "koichi",
      "lastName": "sato",
      "age": 7
    }
  ]
}
Array.isArray(): true
People [
  Person { firstName: 'momotaro', lastName: 'yamada', age: 5 },
  Person { firstName: 'takashi', lastName: 'suzuki', age: 6 },
  Person { firstName: 'koichi', lastName: 'sato', age: 7 }
]
{
  "id": "converted to raw array from extended array using Array.map()",
  "data": {
    "0": {
      "firstName": "momotaro",
      "lastName": "yamada",
      "age": 5
    },
    "1": {
      "firstName": "takashi",
      "lastName": "suzuki",
      "age": 6
    },
    "2": {
      "firstName": "koichi",
      "lastName": "sato",
      "age": 7
    }
  }
}
Array.isArray(): true
[
  Person { firstName: 'momotaro', lastName: 'yamada', age: 5 },
  Person { firstName: 'takashi', lastName: 'suzuki', age: 6 },
  Person { firstName: 'koichi', lastName: 'sato', age: 7 }
]
{
  "id": "converted to raw array from extended array using new Array(...this)",
  "data": [
    {
      "firstName": "momotaro",
      "lastName": "yamada",
      "age": 5
    },
    {
      "firstName": "takashi",
      "lastName": "suzuki",
      "age": 6
    },
    {
      "firstName": "koichi",
      "lastName": "sato",
      "age": 7
    }
  ]
}
Array.isArray(): true
[
  { firstName: 'momotaro', lastName: 'yamada', age: 5 },
  { firstName: 'takashi', lastName: 'suzuki', age: 6 },
  { firstName: 'koichi', lastName: 'sato', age: 7 }
]
{
  "id": "converted to raw array from extended array using JSON.parse(JSON.stringify(this))",
  "data": [
    {
      "firstName": "momotaro",
      "lastName": "yamada",
      "age": 5
    },
    {
      "firstName": "takashi",
      "lastName": "suzuki",
      "age": 6
    },
    {
      "firstName": "koichi",
      "lastName": "sato",
      "age": 7
    }
  ]
}

パターン3がMapになってしまうのが意外でした。ログでDynamoDBに渡すオブジェクトを出力していますが、Peopleクラスの情報が保持されたままになっていますね。 this.map(item => item) とすれば素のArrayが返ると思っていたのですが、そうではなかったようです。まだまだjs力が足りません。

拡張Arrayを保持したままDynamoDBにputするにはパターン4, 5のどちらかを採用すれば良いのですが、パターン5は見た目的にアレなのでパターン4で実装を進めていこうと思います。
(もっと良い方法を知っている人がいればぜひ教えて下さい🥺)