
React演習問題(JSX・コンポーネント・Props・State)
初級問題(9問) JSXの書き方 <div class="container"> <h1 id="title&q […]
前回はMySQLと生SQLを使用してデータベース連携を実装しました。しかし、生SQLにはいくつかの課題があります:
これらの課題を解決するために、今回はPrisma ORMを導入します。Prismaは現代的なTypeScript/JavaScript向けORMで、型安全なデータベース操作を提供します。
Prismaは次の主要コンポーネントから構成されるオープンソースのデータベースツールキットです:
npm install prisma --save-dev
npm install @prisma/client
npx prisma init
このコマンドは以下のファイルを作成します:
prisma/schema.prisma
: Prismaスキーマファイル.env
: 環境変数ファイル(データベース接続URL).env
ファイルを編集してデータベース接続URLを設定します:
# .env
DATABASE_URL="mysql://app_user:password@localhost:3306/todo_app"
prisma/schema.prisma
ファイルを編集してデータモデルを定義します:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Todo {
id Int @id @default(autoincrement())
title String
completed Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("todos")
}
// ユーザーモデル(後の認証機能で使用)
model User {
id Int @id @default(autoincrement())
email String @unique
password String
name String?
todos Todo[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("users")
}
npx prisma migrate dev --name init
このコマンドは:
npx prisma migrate status
// lib/prisma.js
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
});
module.exports = prisma;
前回のMySQL直接操作をPrismaを使用するように書き換えましょう:
// server.js
const express = require('express');
const prisma = require('./lib/prisma'); // Prisma Client
const app = express();
const port = 3000;
app.use(express.json());
// GET /todos - すべてのTODOを取得
app.get('/todos', async (req, res) => {
try {
const todos = await prisma.todo.findMany({
orderBy: { createdAt: 'desc' }
});
res.json(todos);
} catch (error) {
console.error('データ取得エラー:', error);
res.status(500).json({ error: 'データの取得に失敗しました' });
}
});
// GET /todos/:id - 特定のTODOを取得
app.get('/todos/:id', async (req, res) => {
try {
const id = parseInt(req.params.id);
const todo = await prisma.todo.findUnique({
where: { id }
});
if (!todo) {
return res.status(404).json({ error: 'TODOが見つかりません' });
}
res.json(todo);
} catch (error) {
console.error('データ取得エラー:', error);
res.status(500).json({ error: 'データの取得に失敗しました' });
}
});
// POST /todos - 新しいTODOを作成
app.post('/todos', async (req, res) => {
try {
const { title, completed = false } = req.body;
if (!title || title.trim() === '') {
return res.status(400).json({ error: 'タイトルは必須です' });
}
const todo = await prisma.todo.create({
data: {
title: title.trim(),
completed
}
});
res.status(201).json(todo);
} catch (error) {
console.error('データ作成エラー:', error);
res.status(500).json({ error: 'TODOの作成に失敗しました' });
}
});
// PUT /todos/:id - TODOを更新
app.put('/todos/:id', async (req, res) => {
try {
const id = parseInt(req.params.id);
const { title, completed } = req.body;
// 存在確認
const existingTodo = await prisma.todo.findUnique({
where: { id }
});
if (!existingTodo) {
return res.status(404).json({ error: 'TODOが見つかりません' });
}
const todo = await prisma.todo.update({
where: { id },
data: {
title: title !== undefined ? title : existingTodo.title,
completed: completed !== undefined ? completed : existingTodo.completed
}
});
res.json(todo);
} catch (error) {
console.error('データ更新エラー:', error);
res.status(500).json({ error: 'TODOの更新に失敗しました' });
}
});
// DELETE /todos/:id - TODOを削除
app.delete('/todos/:id', async (req, res) => {
try {
const id = parseInt(req.params.id);
// 存在確認
const existingTodo = await prisma.todo.findUnique({
where: { id }
});
if (!existingTodo) {
return res.status(404).json({ error: 'TODOが見つかりません' });
}
await prisma.todo.delete({
where: { id }
});
res.status(204).send();
} catch (error) {
console.error('データ削除エラー:', error);
res.status(500).json({ error: 'TODOの削除に失敗しました' });
}
});
// 高度なクエリの例
app.get('/todos/stats', async (req, res) => {
try {
const total = await prisma.todo.count();
const completed = await prisma.todo.count({
where: { completed: true }
});
const pending = await prisma.todo.count({
where: { completed: false }
});
res.json({
total,
completed,
pending,
completionRate: total > 0 ? (completed / total * 100).toFixed(1) + '%' : '0%'
});
} catch (error) {
console.error('統計取得エラー:', error);
res.status(500).json({ error: '統計の取得に失敗しました' });
}
});
// ページネーションの例
app.get('/todos/paginated', async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const [todos, total] = await Promise.all([
prisma.todo.findMany({
skip,
take: limit,
orderBy: { createdAt: 'desc' }
}),
prisma.todo.count()
]);
res.json({
todos,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
hasNext: page < Math.ceil(total / limit),
hasPrev: page > 1
}
});
} catch (error) {
console.error('ページネーションエラー:', error);
res.status(500).json({ error: 'データの取得に失敗しました' });
}
});
// エラーハンドリングミドルウェア
app.use((error, req, res, next) => {
console.error('予期せぬエラー:', error);
res.status(500).json({ error: 'サーバー内部エラーが発生しました' });
});
// アプリケーション終了時のPrisma接続クローズ
process.on('beforeExit', async () => {
await prisma.$disconnect();
});
app.listen(port, () => {
console.log(`TODO APIサーバーが起動しました: http://localhost:${port}`);
});
// ユーザーとそのTODOを一緒に取得
app.get('/users/:userId/todos', async (req, res) => {
try {
const userId = parseInt(req.params.userId);
const userWithTodos = await prisma.user.findUnique({
where: { id: userId },
include: {
todos: {
orderBy: { createdAt: 'desc' }
}
}
});
if (!userWithTodos) {
return res.status(404).json({ error: 'ユーザーが見つかりません' });
}
res.json(userWithTodos);
} catch (error) {
console.error('ユーザーTODO取得エラー:', error);
res.status(500).json({ error: 'データの取得に失敗しました' });
}
});
// トランザクションの例
app.post('/todos/batch', async (req, res) => {
try {
const { todos } = req.body;
const result = await prisma.$transaction(async (tx) => {
const createdTodos = [];
for (const todoData of todos) {
const todo = await tx.todo.create({
data: {
title: todoData.title,
completed: todoData.completed || false
}
});
createdTodos.push(todo);
}
return createdTodos;
});
res.status(201).json(result);
} catch (error) {
console.error('一括登録エラー:', error);
res.status(500).json({ error: '一括登録に失敗しました' });
}
});
// 日別のTODO作成統計
app.get('/todos/daily-stats', async (req, res) => {
try {
const dailyStats = await prisma.todo.groupBy({
by: ['createdAt'],
_count: {
id: true
},
where: {
createdAt: {
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // 過去30日間
}
},
orderBy: {
createdAt: 'asc'
}
});
res.json(dailyStats);
} catch (error) {
console.error('統計取得エラー:', error);
res.status(500).json({ error: '統計の取得に失敗しました' });
}
});
Prismaに付属するGUIツールでデータベースを可視化できます:
npx prisma studio
このコマンドを実行すると、ブラウザでhttp://localhost:5555
が開き、データの閲覧や編集が可能になります。
# スキーマ変更後のマイグレーション
npx prisma migrate dev --name add_description_field
# プロダクション環境でのマイグレーション
npx prisma migrate deploy
# Prisma Clientの再生成(スキーマ変更後)
npx prisma generate
# データベースのリセット(開発環境用)
npx prisma migrate reset
Prismaを使用することで、型安全で直感的なデータベース操作が可能になりました。次回は、Fetch APIとAxiosを使用して、ReactフロントエンドからこのAPIに接続する方法を学びます。これにより、フロントエンドとバックエンドの完全な連携が実現します。
Prisma ORMの導入により、以下のメリットを得ました:
Prismaは現代的なWebアプリケーション開発に不可欠なツールであり、TypeScriptとの相性も抜群です。次回はこのAPIをReactアプリケーションから呼び出す方法を学びましょう。