😸

Amplifyで簡単にDB設計をしよう!~テーブル定義の巻~

2023/09/25に公開

Amplifyは、GraphQLスキーマの中で@modelというディレクティブを使うことで、あっという間にAmazon DynamoDBのテーブルを作成してくれる優れもの。さらに、@hasOneや@hasMany、@belongsTo、@manyToManyなどのディレクティブを駆使することで、データモデル間の関係も簡単に表現できちゃいます!
とりあえず今回は以下の構成で紹介していきます!

  • テーブル定義
  • 主キーの設定
  • セカンダリインデックスの設定

前提知識

もし、DynamoDBに触れたことがないという方いたら、以下の記事等を読んでおくことをお勧めします。ドキュメントだけ読めばDBの実装はできますが、その実装の意味をちゃんと理解したり最適な設計していくのにはそれ以前の概念的理解が大事になってきます!

https://dev.classmethod.jp/referencecat/conceptual-learning-about-dynamodb/
↑分かりやすくて好きなのですが、若干古いのでハッシュキーをプライマリキーに置き換えて読めばいけると思います!
https://speakerdeck.com/_kensh/dynamodb-design-practice
https://qiita.com/_kensh/items/2351096e6c3bf431ff6f

また、GraphQLも理解しておく必要があるので、基礎知識をつけておくことをお勧めします!
https://zenn.dev/nameless_sn/articles/graphql_tutorial

テーブル定義

例として、"Todo"というテーブルを作成してみましょう。@modelディレクティブを使うと、idフィールドも自動的に主キーとして追加されます。主キーのカスタマイズの仕方は、後ほど解説します!

type Todo @model {
  content: String
}

そして、amplify pushを実行するだけで、Amplify CLIが以下のような魔法をかけてくれます✨

  • "Todo"テーブルの生成
    • id, createdAt, updatedAtというフィールドが自動的に追加されます
    • ちなみに、createdAtとupdatedAtは読み取り専用ですよ!
    • 詳しくはこちら
  • TodoテーブルにアクセスするためのGraphQL APIの生成
    • CRUD操作やリスト表示が可能に!

Todoをリストアップする方法

すべてのTodoをリストアップするためのクエリ定義と、aws-amplify1(クライアントライブラリ)を使ったクエリ実行のサンプルコードをご紹介します!
クエリ定義はAmplifyが自動で生成してくれます
クエリ定義

query QueryAllTodos {
  listTodos() {
    todos {
      items {
        id
        content
        createdAt
        updatedAt
      }
    }
  }
}

クエリ実行

import { Amplify, API, graphqlOperation } from 'aws-amplify';
import awsconfig from './aws-exports';
import { listTodos } from './graphql/queries';

Amplify.configure(awsconfig);

try {
    const result = await API.graphql(graphqlOperation(listTodos));
    const todos = result.data.listTodos;
} catch (res) {
    const { errors } = res;
    console.error(errors);
}

以上、Amplifyを使ったDynamoDBテーブルの定義についてご紹介しました!ここからは、テーブルを構成するキーの設定方法を解説していきます!

主キーの設定

Amplifyの@modelディレクティブを使ったGraphQLタイプは、デフォルトでidフィールドが主キーとして設定されているんですよ。でも、魔法の呪文@primaryKeyディレクティブを使えば、このデフォルトの動作をパッと変えることができるのです!ただ、@primaryKeyを使うための条件として、フィールドは必須項目でなければなりません。

例えば、こんな感じでtodoIdを主キーとして指定することができます。

type Todo @model {
  todoId: ID! @primaryKey
  content: String
}

@primaryKeyを使うと、指定したフィールドがテーブルの主キーとして割り当てられます。DynamoDBの世界では、この主キーを「Partition key」として知られています。そして、Todoをクエリする際、このPartition keyのフィールド(今回の例ではtodoId)とピッタリ合致するものだけが選ばれるのです。

ソートキーの導入

さらに、DynamoDBには「Sort key」というものもあります。これを使うと、異なるフィールドの組み合わせで主キーを指定でき、データのソートやフィルタリングがもっと楽しく、もっと効果的に!

例えば、こんなInventoryというテーブルを考えてみましょう

type Inventory @model {
  productID: ID! @primaryKey(sortKeyFields: ["warehouseID"])
  warehouseID: ID!
  InventoryAmount: Int!
}

このテーブルでは、productIDとwarehouseIDを組み合わせて、複合主キーとして活用しています。これを Composite primary key(複合主キー) と呼びます。複合主キーを使うと、複数のフィールドを組み合わせて、もっと柔軟なクエリを行えるのです!

このスキーマを使うと、特定の商品IDと倉庫IDの組み合わせで、在庫情報をクエリすることができます。
以下はそのためのamplifyが生成してくれるクエリ定義です。

query QueryInventoryByProductAndWarehouse($productID: ID!, $warehouseID: ID!) {
  getInventory(productID: $productID, warehouseID: $warehouseID) {
    productID
    warehouseID
    inventoryAmount
  }
}

次に、以下のコードを使用して、GraphQLクエリを実行します:

import { getInventory } from './graphql/queries';

const params = {
    productID: 'product-id',
    warehouseID: 'warehouse-id',
};

const result = await API.graphql(graphqlOperation(getInventory, params));
const inventory = result.data.getInventory;

このコードは、指定された商品IDと倉庫IDを使用して、正確な在庫情報を取得する方法を示しています。もし何かエラーが発生した場合、コンソールのエラーメッセージの表示も、ライブラリが実装してくれています

セカンダリインデックスの設定

Amazon DynamoDBでは、secondary indexes を使用してアクセスパターンを最適化することができるんです。それは、@indexディレクティブ!

セカンダリインデックスは、"primary key"とオプションで"sort key"から構成されます。"primary key"でデータの厳密な等価性を確認し、"sort key"を使って、さまざまな条件(gt、ge、lt、le、eq、beginsWith、betweenなど)でデータを検索できるんです。

例えば、こんな感じでCustomerというテーブルを作成してみましょう。

type Customer @model {
  id: ID!
  name: String!
  phoneNumber: String
  accountRepresentativeID: ID! @index
}

そして、以下のクエリ定義が生成され、accountRepresentativeIDに基づいて「Customer」レコードを検索することができます。

query QueryCustomersForAccountRepresentative($accountRepresentativeID: ID!) {
  customersByAccountRepresentativeID(accountRepresentativeID: $accountRepresentativeID) {
    customers {
      items {
        id
        name
        phoneNumber
      }
    }
  }
}

次に、以下のコードを使用して、GraphQLクエリを実行します:

import { customersByAccountRepresentativeID } from './graphql/queries';

const params = {
    accountRepresentativeID: 'account-rep-id',
};

const result = await API.graphql(
    graphqlOperation(customersByAccountRepresentativeID, params)
);
const customers = result.data.customersByAccountRepresentativeID;

このコードでは、特定のアカウント担当者IDに基づいて顧客情報を取得できます。先程も述べたように、エラーハンドリングはライブラリ内で実装済みです

セカンダリインデックスのカスタマイズ

GraphQLクエリ名やセカンダリインデックス名をカスタマイズできます。例えば、こんな風にCustomerテーブルをカスタマイズしてみましょう。

type Customer @model {
  id: ID!
  name: String!
  phoneNumber: String
  accountRepresentativeID: ID! @index(name: "byRepresentative", queryField: "customerByRepresentative")
}

name=インデックス名、queryField=クエリ名をカスタマイズしています!

以下の例のクエリが定義され、特定の担当者IDに基づいて「Customer」レコードを検索できるようになります:

query QueryCustomersForAccountRepresentative($representativeId: ID!) {
  customerByRepresentative(accountRepresentativeID: $representativeId) {
    customers {
      items {
        id
        name
        phoneNumber
      }
    }
  }
}

次に、以下のコードを使用して、GraphQLクエリを実行します:

import { customerByRepresentative } from './graphql/queries';

const params = {
    accountRepresentativeID: 'account-rep-id',
};

const result = await API.graphql(
    graphqlOperation(customerByRepresentative, params)
);
const customer = result.data.customerByRepresentative;

このコードは、特定のアカウント担当者IDに基づいて顧客情報を取得できます。エラーが発生した場合、エラーメッセージがコンソールに表示されます。

ソートキーを設定すると、データの検索がもっと柔軟になります!。sortKeyFieldsパラメータにフィールドを追加するだけで、データのソートやフィルタリングができるようになります。

type Customer @model @auth(rules: [{ allow: public }]) {
  id: ID!
  name: String! @index(name: "byNameAndPhoneNumber", sortKeyFields: ["phoneNumber"], queryField: "customerByNameAndPhone")
  phoneNumber: String
  accountRepresentativeID: ID! @index
}

以下の例のクエリが生成され、名前に基づいて「Customer」をクエリし、phoneNumberに基づいてフィルタリングすることができるようになります:

query MyQuery {
  customerByNameAndPhone(phoneNumber: { beginsWith: "+1" }, name: "Rene") {
    items {
      id
      name
      phoneNumber
    }
  }
}

次に、以下のコードを使用して、GraphQLクエリを実行します:

import { customerByNameAndPhone } from './graphql/queries';

const params = {
    phoneNumber: { beginsWith: '+1' },
    name: 'Rene',
};

const result = await API.graphql(
    graphqlOperation(customerByNameAndPhone, params)
);
const customer = result.data.customerByNameAndPhone;

このコードは、特定の名前と電話番号の接頭辞に基づいて顧客情報を取得できます。エラーが発生した場合、エラーメッセージがコンソールに表示されます。

まとめ

最後まで読んでいただきありがとうございました!実装自体は一度理解してしまえば簡単ですが、DynamoDBの概念理解がないとちゃんと運用できないですし、GraphQL Transformerの構文が大幅に変化して混乱するという事もありますのでご注意を!
次回はリレーションについて説明していくのでお楽しみに~
https://zenn.dev/maromero/articles/adcab396356774

Discussion