SvelteKitでPrismaを使って、MySQLデータベースにTODOを保存するアプリケーションを作ってみました。
SvelteKitのサーバーサイド、クライアントサイドの基本と、Prismaの組み合わせの基礎になると思います。
対象読者
- SvelteKitでSSRアプリを作りたい方
- MySQLと連動するSSRアプリを作りたい方
- TypeScriptでSvelteKitアプリケーションを作成したい方
開発環境
- SvelteKit v1.27
- MySQL v8
- Prisma v5.7
- TypeScript v5.0
Prismaを利用する前の準備
prisma migrate devコマンドを実行するとshadow databaseと呼ばれる一時的なデータベースがマイグレーションのために作成されます。そのため、テーブルを作成する権限を付与しておく必要があります。
なお、CREATE, ALTER, DROP, REFERENCESの権限は強力なので開発環境以外では設定しないようにしましょう。
DROP DATABASE IF EXISTS todo_db;
CREATE DATABASE todo_db;
# ユーザー作成
CREATE USER IF NOT EXISTS user IDENTIFIED BY 'hogehoge';
GRANT ALL PRIVILEGES ON todo_db.* to user;
# CREATE、ALTER、DROP、REFERENCESの権限を付与
GRANT create, alter, drop, references ON *.* to user;
権限を付与していないと、マイグレーション時に下記のようなエラーが表示されてしまいます。
Prisma Migrate could not create the shadow database. Please make sure the database user has permission to create databases. Read more about the shadow database (and workarounds) at https://pris.ly/d/migrate-shadow
Original error: Error code: P1010
Prismaの利用手順
Prismaを開発時に使うために必要なライブラリをインストールします。
プロジェクトフォルダ内でインストールするだけならば、-D
オプションをつけます。
npm install prisma -D
Prismaの初期化コマンドを実行します。
npx prisma init
初期化コマンドによって、prismaフォルダが作成され、スキーマファイルの雛形を生成します。
データーベースへの接続情報を記述する、.envファイルも作成されます。
まずはスキーマファイルの編集をします。
今回はデータベースをMySQLに変更するのと、テーブルtodosの定義を行います。
todosテーブルはid, body, createdAtカラムを持つテーブルにしています。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql" // MySQLを使うので編集
url = env("DATABASE_URL")
}
// テーブルの定義を行う
model todos {
id Int @id @default(autoincrement())
body String
createdAt DateTime @default(now())
}
.envファイルにはMySQL接続文字列を指定します。
DATABASE_URL="mysql://user:P@ssw0rd@localhost:3306/todo_db"
データベースへの接続設定と、テーブルの定義ができたら、マイグレートを行い、MySQLにテーブルを作成します。
npx prisma migrate dev
SvelteKitのプログラムからPrismaを使って接続するために、Prismaクライアントをインストールします。
npm install @prisma/client
インストール時に、スキーマファイルの内容から、、node_modules/.prisma/clientフォルダにデータアクセスを行うためのヘルパーコードや型定義が生成されます。
SvelteKit側のプログラムを行う
サーバーサイド側の処理から作成します。SvelteKitではサーバー側の処理は+page.server.tsファイルに記述します。
Formからの送信を受け取る
クライアントサイドからFormで送信された内容を受け取るときは、Actions型のオブジェクトを準備します。
Actions型オブジェクトに送信された時のクエリパラメータ名のプロパティを設定し、送信を受け取る処理を記述します。
import type { Actions } from "./$types";
export const actions : Actions = {
default: async ({request}) => {
// 送信を受け取る処理
}
};
default
プロパティはクエリパラメータが無い場合に使います。
プロパティの値はasyncキーワードをつけたメソッドにし、非同期処理を記述していきます。
送信された内容は、引数のrequestで受け取ります。
requestの中身は送信されたname属性名と、送信内容がセットになったオブジェクトになっています。
{ name: 'body', value: 'hogehoge' }
そのため、bodyの送信内容を受け取る場合はformDataメソッドを使って、FormData型のオブジェクトに変換する必要があります。
formDataメソッドは非同期処理なので、awaitキーワードをつけています。
const data = await request.formData();
FormData型のオブジェクトにはgetメソッドが用意されており、name属性名を引数で渡すことで、入力値を受け取れます。
const body = data.get('body');
getメソッドの戻り値はFormDataEntryValue型と、nullのユニオン型になっています。
body変数にはnull又は入力値が代入されます。
そのため、型チェックの処理がないと、コードエディタ上で警告が出てしまいます。
if (typeof body !== "string" || !body) {
return fail(400, { message: "TODO内容は必須です。" })
}
body変数がstring型ではなく、かつ空(null)だったらば、fail関数でエラーレスポンスを返すようにしています。
これでFormから送信された入力値を受け取れるようになりました。
入力値をデータベースに登録しましょう。
送信データをPrismaを使って登録する
そのためにはPrismaClientインスタンスを生成します。
import {PrismaClient} from '@prisma/client';
const prisma = new PrismaClient({log: ['query', 'info']});
PrismaClientインスタンス内にはテーブル定義を行ったテーブル名のプロパティや、データベースを操作するメソッドなどが内包されています。
送信内容を受けとった処理の後に、MySQLへの登録処理を記述します。
await prisma.todos.create({
data: {
body
}
});
todosはテーブル定義した時のテーブル名が、PrismaClientインスタンス生成時に同じ名前のプロパティが自動的に作られます。
createメソッドはインサートSQL文を生成します。
createメソッドの引数にはオブジェクトを指定し、dataプロパティにテーブルのカラム名と同じプロパティを持つオブジェクトを指定します。
今回はtodosテーブルにはbodyカラムを定義しているので、bodyプロパティに、body変数の値を設定しています。
最後に送信処理が成功したので、成功レスポンスを返します。
return {
success: true
};
データベースから全件検索する
todosテーブルに登録されたレコードを全て取得し、クライアント側で利用できるようにします。
SvelteKitが用意しているPageServerLoad型をインポートしておきます。
import type { Actions, PageServerLoad } from "./$types";
load関数を定義します。
load関数はクライアント側のページを生成する前に実行される関数です。
ページ表示前におこないたい処理を記述します。
export const load: PageServerLoad = ({params}) => {
const todos = prisma.todos.findMany();
return {
todos
};
};
今回はPrismaインスタンからfindManyメソッドを使い全件取得します。
todosテーブルのすべてのレコードが取得され、配列で返されます。
取得した配列をtodosプロパティにセットし、returnします。
returnされたデータはクライアント側のプログラムでdata変数に乗せられます。
完成形です。
import type { Actions, PageServerLoad } from "./$types";
import { fail } from "@sveltejs/kit";
import {PrismaClient} from '@prisma/client';
const prisma = new PrismaClient({log: ['query', 'info']});
export const load: PageServerLoad = ({params}) => {
const todos = prisma.todos.findMany();
return {
todos
};
};
export const actions : Actions = {
default: async ({request}) => {
const data = await request.formData();
const body = data.get('body');
if (typeof body !== "string" || !body) {
return fail(400, { message: "TODO内容は必須です。" })
}
await prisma.todos.create({
data: {
body
}
});
return {
success: true
};
}
};
クライアント側の処理
SvelteKit側で提供している、2つの型をインポートします。
import type { ActionData } from './$types';
import type { PageData } from "./$types";
ActionData型はサーバーサイドに送信した結果を取得する型です。
+page.server.tsのactionsオブジェクトで、指定したレスポンスを受け取ってくれます。
PageData型は+page.server.tsのload関数でreturnしたデータを受け取る型です。
インポートができたら、exportキーワードをつけて変数を準備します。
exportキーワードをつけることで、テンプレート(HTML)で変数が参照できるようになります。
<script lang="ts">
import type { ActionData } from './$types';
import type { PageData } from "./$types";
export let data: PageData;
export let form: ActionData;
</script>
テンプレート部分
テンプレート側を作成します。
<h1>Todo App</h1>
{#if form?.success}
<p class="success">Todo added!</p>
{/if}
{#if form?.message}
<p class="error">{form?.message}</p>
{/if}
<form method="post" use:enhance>
<div><input type="text" name="body" id="body"></div>
<div><button>追加</button></div>
</form>
<hr>
<ul>
{#each data.todos as todo}
<li>{todo.body} ({todo.createdAt.toLocaleString('ja')})</li>
{/each}
</ul>
<style>
.error {
color: red;
}
.success {
color: green;
}
</style>
ロジックブロックを使って、送信成功、失敗のメッセージを制御します。
{#if form?.success}
<p class="success">Todo added!</p>
{/if}
{#if form?.message}
<p class="error">{form?.message}</p>
{/if}
formオブジェクトのsuccessプロパティが存在すれば、登録成功のメッセージを表示しています。
エラー時はformオブジェクトにmessageプロパティが設定されるので、ifブロックを使って、失敗時のメッセージを表示しています。
この時、formオブジェクトが存在しない可能性もあるので、null安全のために変数名?
としています。
全件検索した結果をeachブロックを使って表示します。
PageData型のdata変数に、+page.server.tsのload関数でreturnした配列が代入されています。
<ul>
{#each data.todos as todo}
<li>{todo.body} ({todo.createdAt.toLocaleString('ja')})</li>
{/each}
</ul>
完成系です。
<script lang="ts">
import type { ActionData } from './$types';
import type { PageData } from "./$types";
export let data: PageData;
export let form: ActionData;
</script>
<h1>Todo App</h1>
{#if form?.success}
<p class="success">Todo added!</p>
{/if}
{#if form?.message}
<p class="error">{form?.message}</p>
{/if}
<form method="post">
<div><input type="text" name="body" id="body"></div>
<div><button>追加</button></div>
</form>
<hr>
<ul>
{#each data.todos as todo}
<li>{todo.body} ({todo.createdAt.toLocaleString('ja')})</li>
{/each}
</ul>
<style>
.error {
color: red;
}
.success {
color: green;
}
</style>
結論
SvelteKitを使うことで、Webアプリケーションが作成できます。
特に大きなメリットは、JavaScriptのコードでfetch関数を使ってクライアントからサーバーサイドに渡すなどの処理をSvelteKit側に任せることができます。
また、サーバーサイドで取得したデータをクライアント側に渡す処理もPageData型の変数を用意するだけで済みます。
クライアント側と、サーバーサイド側の処理を記述するコード量が減るのが大きなメリットと思います。
PrismaもORMとして、かなり優秀で、他言語のORMに慣れている方だと、違和感なく利用できると思います。
また、Prismaはマイグレーションファイルで定義したテーブル構造を型として持ってくれます。
そのため、TypeScriptの静的型付け言語と相性が大変良いです。
今後はTypeScriptで開発することが増えてくると思いますので、そういった意味ではPrismaは今後活躍する場が増えるのではないかと思います。