🪄

AWS Amplify Gen2でRDS(MySQL)のCRUD操作を行う

2024/06/06に公開

はじめに

一覧を表示(読み込み)、登録と削除

更新

事前にRDSで下記のDB(myapp)とテーブル(users)が作成されているものとします。
フロントはNext.js("next": "13.5.4")です。

SET CHARSET UTF8;

CREATE DATABASE IF NOT EXISTS myapp;
USE myspp;

CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    first_name VARCHAR(255) NOT NULL,
    last_name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP
);

INSERT INTO users (first_name, last_name) VALUES ('山田', '太郎'), ('佐藤', '花子');

シークレット設定

RDSインスタンスが以下の場合は
ホスト名: mydatabase.c12345abcde.ap-northeast-1.rds.amazonaws.com
ポート: 3306
データベース名: myapp
ユーザー名: admin
パスワード: password

MySQLの接続文字列形式は下記のようになります。

mysql://admin:password@mydatabase.c12345abcde.ap-northeast-1.rds.amazonaws.com:3306/myapp

下記コマンドを実行すると、接続文字列の入力が求められますので登録してください。

npx ampx sandbox secret set SQL_CONNECTION_STRING

設定内容を間違えた場合は、下記を参考に設定し直してください。
https://zenn.dev/nenenemo/articles/9fa30ef05ffb4d#サンドボックスの秘密情報の管理

データベーススキーマの生成

シークレットをもとにamplify/data/schema.sql.tsというデータベーススキーマを生成します。

npx ampx generate schema-from-database --connection-uri-secret SQL_CONNECTION_STRING --out amplify/data/schema.sql.ts

生成したスキーマは編集せずにamplify/data/resource.tsでインポートして使用してください。

バックエンドデータアクセスの設定

下記のように作成します。

amplify/data/resource.ts
import { defineData, type ClientSchema } from '@aws-amplify/backend';
import { schema as generatedSqlSchema } from './schema.sql';

const schema = generatedSqlSchema
  .authorization((allow) => allow.authenticated())
  .renameModels(() => [['users', 'Users']]); 

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool',
  },
});

フロントの作成

app/page.tsx
'use client';
import type { Schema } from '@/amplify/data/resource';
import outputs from '@/amplify_outputs.json';
import { Authenticator, ThemeProvider } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import { Amplify } from 'aws-amplify';
import { generateClient } from 'aws-amplify/data';
import { useEffect, useState } from 'react';
import styles from './page.module.css';

Amplify.configure(outputs);

const client = generateClient<Schema>();

export default function App() {
  const [users, setUsers] = useState<Array<Schema['Users']['type']>>([]);

  const listUsers = () => {
    const sub = client.models.Users.observeQuery().subscribe({
      next: (data) => {
        setUsers([...data.items]);
      },
      error: (err) => {
        console.error('ユーザーの取得に失敗しました。:', err);
      },
    });
    return () => sub.unsubscribe();
  };

  useEffect(() => {
    listUsers();
  }, []);

  const createUser = () => {
    const firstName = window.prompt('姓を入力してください。');
    const lastName = window.prompt('名を入力してください。');
    if (firstName && lastName) {
      client.models.Users.create({
        id: 3,
        first_name: firstName,
        last_name: lastName,
      })
        .then(() => listUsers())
        .catch((error) =>
          console.error('ユーザー登録ができませんでした。:', error)
        );
    }
  };

  const updateUser = (id: number) => {
    const updateFirstName = window.prompt('姓を入力してください。');
    const updateLastName = window.prompt('名を入力してください。');

    if (updateFirstName && updateLastName) {
      client.models.Users.update({
        id: id,
        first_name: updateFirstName,
        last_name: updateLastName,
      });
      setUsers(users.filter((user) => user.id !== id));
    }
  };

  const deleteUser = (id: number) => {
    client.models.Users.delete({ id });
    setUsers(users.filter((user) => user.id !== id));
  };

  return (
    <ThemeProvider>
      <Authenticator>
        {({ signOut, user }) => (
          <main>
            <h1>{user?.signInDetails?.loginId}で ログインしています。</h1>
            <h1>ユーザーリスト</h1>
            <button onClick={createUser} className={styles['button']}>
              ユーザーを作成する
            </button>
            <ul>
              {users.map((item) => (
                <li key={item.id} className={styles['list-item']}>
                  <span className={styles['name']}>
                    {item.first_name} {item.last_name}
                  </span>
                  <button
                    onClick={() => updateUser(item.id)}
                    className={styles['button']}
                  >
                    Update
                  </button>
                  <button
                    onClick={() => deleteUser(item.id)}
                    className={styles['button']}
                  >
                    Delete
                  </button>
                </li>
              ))}
            </ul>
            <button onClick={signOut} className='button'>
              Sign out
            </button>
          </main>
        )}
      </Authenticator>
    </ThemeProvider>
  );
}

Window.prompt()

ユーザーにテキスト入力を求めるダイアログボックスを表示します。

第一引数はユーザーに表示するメッセージ、第二引数はテキスト入力フィールドのデフォルト値(省略可能)です。
ユーザーが「OK」をクリックすると、入力されたテキストが関数から返されます。ユーザーが「キャンセル」をクリックすると、nullが返されます。
https://developer.mozilla.org/ja/docs/Web/API/Window/prompt

スタイル

app/page.module.css
.name {
  margin-right: auto;
}
.list-item {
  display: flex;
  align-items: center;
}
.button {
  margin-right: 10px;
}

不明な点

id INT AUTO_INCREMENT PRIMARY KEYという設定にも関わらず、手動でidを設定しないとエラーが発生してしまい、解決方法が分からなかったです。(解決法として、データの数を数えて次のidとして+1する方法する方法では、同時にアクセスがあった時にエラーになってしまう...)

個別にSQLを実行する場合の記述がわからなかったので、ご存知の方は教えていただけると助かります。

参考にさせていただきました

https://aws.amazon.com/jp/blogs/news/new-in-aws-amplify-integrate-with-sql-databases-oidc-saml-providers-and-the-aws-cdk/
https://zenn.dev/ototrip/articles/tech-nextjs-amplify-3

https://docs.amplify.aws/nextjs/build-a-backend/data/connect-to-existing-data-sources/connect-postgres-mysql-database/
https://dev.classmethod.jp/articles/amplify-gen2-data-mysql/

Property 'addToSchema' does not exist on type 'RDSModelSchema

addToSchemaを使用してカスタムクエリを設定しようとするとエラーになりました。
https://github.com/aws-amplify/amplify-category-api/issues/2530

終わりに

何かありましたらお気軽にコメント等いただけると助かります。
ここまでお読みいただきありがとうございます🎉

Discussion