はじめに
現代のWebアプリケーション開発において、データベースは不可欠な要素です。このチュートリアルでは、Node.jsアプリケーションで最も人気のある3つのデータベース技術について学びます。
このチュートリアルで学べること
- MongoDB(NoSQL)とMongooseを使ったドキュメント指向データベースの操作
- PostgreSQL(SQL)とSequelizeを使ったリレーショナルデータベースの管理
- Redisを使った高速キャッシングシステムの実装
- データベース設計の原則とベストプラクティス
- 本番環境でのパフォーマンス最適化とスケーリング
データベースの選択基準
MongoDB
- スキーマレスで柔軟なデータ構造
- 水平スケーリングが容易
- JSONライクなドキュメント形式
- リアルタイムアプリケーションに最適
PostgreSQL
- ACID準拠の強力なトランザクション
- 複雑なクエリと結合に優れる
- データ整合性が重要な場合に最適
- 豊富な拡張機能とデータ型
Redis
- インメモリデータストア
- 超高速な読み書き性能
- セッション管理やキャッシュに最適
- Pub/Sub機能でリアルタイム通信
MongoDB と Mongoose
MongoDBは最も人気のあるNoSQLデータベースで、Mongooseはそのための強力なODM(Object Document Mapper)です。
1. セットアップ
# MongoDBのインストール(macOS)
brew tap mongodb/brew
brew install mongodb-community
# MongoDBの起動
brew services start mongodb-community
# Mongooseのインストール
npm install mongoose
2. 接続設定
config/database.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/myapp', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
// 接続イベントのリスナー
mongoose.connection.on('error', err => {
console.error('MongoDB connection error:', err);
});
mongoose.connection.on('disconnected', () => {
console.log('MongoDB disconnected');
});
// グレースフルシャットダウン
process.on('SIGINT', async () => {
await mongoose.connection.close();
process.exit(0);
});
} catch (error) {
console.error('Error connecting to MongoDB:', error);
process.exit(1);
}
};
module.exports = connectDB;
3. スキーマとモデルの定義
models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, '名前は必須です'],
trim: true,
maxlength: [50, '名前は50文字以内で入力してください']
},
email: {
type: String,
required: [true, 'メールアドレスは必須です'],
unique: true,
lowercase: true,
validate: {
validator: function(v) {
return /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(v);
},
message: '有効なメールアドレスを入力してください'
}
},
password: {
type: String,
required: [true, 'パスワードは必須です'],
minlength: [6, 'パスワードは6文字以上で入力してください'],
select: false // デフォルトでパスワードを取得しない
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
profile: {
bio: String,
avatar: String,
social: {
twitter: String,
facebook: String,
linkedin: String
}
},
posts: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Post'
}],
createdAt: {
type: Date,
default: Date.now
},
lastLogin: Date,
isActive: {
type: Boolean,
default: true
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// インデックスの設定
userSchema.index({ email: 1 });
userSchema.index({ createdAt: -1 });
userSchema.index({ name: 'text', 'profile.bio': 'text' });
// 仮想プロパティ
userSchema.virtual('fullName').get(function() {
return this.name;
});
userSchema.virtual('postCount').get(function() {
return this.posts.length;
});
// ミドルウェア - 保存前にパスワードをハッシュ化
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// インスタンスメソッド
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
userSchema.methods.toJSON = function() {
const obj = this.toObject();
delete obj.password;
delete obj.__v;
return obj;
};
// 静的メソッド
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email: email.toLowerCase() });
};
userSchema.statics.findActiveUsers = function() {
return this.find({ isActive: true });
};
const User = mongoose.model('User', userSchema);
module.exports = User;
models/Post.js
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: [true, 'タイトルは必須です'],
trim: true,
maxlength: [100, 'タイトルは100文字以内で入力してください']
},
slug: {
type: String,
unique: true,
lowercase: true
},
content: {
type: String,
required: [true, '内容は必須です']
},
excerpt: {
type: String,
maxlength: [200, '抜粋は200文字以内で入力してください']
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
category: {
type: String,
required: true,
enum: ['技術', 'ライフスタイル', 'ビジネス', 'その他']
},
tags: [{
type: String,
lowercase: true
}],
images: [{
url: String,
caption: String
}],
status: {
type: String,
enum: ['draft', 'published', 'archived'],
default: 'draft'
},
views: {
type: Number,
default: 0
},
likes: [{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
createdAt: {
type: Date,
default: Date.now
}
}],
comments: [{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
content: String,
createdAt: {
type: Date,
default: Date.now
}
}],
publishedAt: Date,
metadata: {
readTime: Number,
seoTitle: String,
seoDescription: String,
seoKeywords: [String]
}
}, {
timestamps: true
});
// インデックス
postSchema.index({ slug: 1 });
postSchema.index({ author: 1, status: 1 });
postSchema.index({ tags: 1 });
postSchema.index({ title: 'text', content: 'text' });
// 保存前にスラッグを生成
postSchema.pre('save', function(next) {
if (this.isModified('title')) {
this.slug = this.title
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
}
// 抜粋を自動生成
if (!this.excerpt && this.content) {
this.excerpt = this.content.substring(0, 197) + '...';
}
// 読了時間を計算(1分あたり200文字)
if (this.content) {
this.metadata.readTime = Math.ceil(this.content.length / 200);
}
next();
});
// 公開時に日付を設定
postSchema.pre('save', function(next) {
if (this.isModified('status') && this.status === 'published' && !this.publishedAt) {
this.publishedAt = new Date();
}
next();
});
// メソッド
postSchema.methods.incrementViews = async function() {
this.views += 1;
return this.save();
};
postSchema.methods.addComment = async function(userId, content) {
this.comments.push({ user: userId, content });
return this.save();
};
postSchema.methods.toggleLike = async function(userId) {
const likeIndex = this.likes.findIndex(like =>
like.user.toString() === userId.toString()
);
if (likeIndex > -1) {
this.likes.splice(likeIndex, 1);
} else {
this.likes.push({ user: userId });
}
return this.save();
};
// 静的メソッド
postSchema.statics.findPublished = function() {
return this.find({ status: 'published' })
.sort('-publishedAt')
.populate('author', 'name email');
};
postSchema.statics.findBySlug = function(slug) {
return this.findOne({ slug })
.populate('author', 'name email profile.avatar');
};
postSchema.statics.searchPosts = function(query) {
return this.find(
{ $text: { $search: query } },
{ score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } });
};
const Post = mongoose.model('Post', postSchema);
module.exports = Post;
4. CRUD操作の実装
controllers/userController.js
const User = require('../models/User');
const catchAsync = require('../utils/catchAsync');
const AppError = require('../utils/appError');
// ユーザー一覧取得
exports.getUsers = catchAsync(async (req, res, next) => {
// クエリビルダー
const query = User.find();
// フィルタリング
if (req.query.role) {
query.where('role').equals(req.query.role);
}
if (req.query.active) {
query.where('isActive').equals(req.query.active === 'true');
}
// 検索
if (req.query.search) {
query.where('name').regex(new RegExp(req.query.search, 'i'));
}
// ソート
const sortBy = req.query.sortBy || '-createdAt';
query.sort(sortBy);
// ページネーション
const page = parseInt(req.query.page, 10) || 1;
const limit = parseInt(req.query.limit, 10) || 10;
const skip = (page - 1) * limit;
query.skip(skip).limit(limit);
// フィールド選択
if (req.query.fields) {
const fields = req.query.fields.split(',').join(' ');
query.select(fields);
}
// ポピュレート
query.populate('posts', 'title status publishedAt');
// 実行
const users = await query;
// 総数を取得
const total = await User.countDocuments();
res.json({
status: 'success',
results: users.length,
total,
page,
pages: Math.ceil(total / limit),
data: users
});
});
// 単一ユーザー取得
exports.getUser = catchAsync(async (req, res, next) => {
const user = await User.findById(req.params.id)
.populate({
path: 'posts',
select: 'title slug status publishedAt views',
match: { status: 'published' },
options: { sort: '-publishedAt', limit: 10 }
});
if (!user) {
return next(new AppError('ユーザーが見つかりません', 404));
}
res.json({
status: 'success',
data: user
});
});
// ユーザー作成
exports.createUser = catchAsync(async (req, res, next) => {
const newUser = await User.create({
name: req.body.name,
email: req.body.email,
password: req.body.password,
role: req.body.role
});
res.status(201).json({
status: 'success',
data: newUser
});
});
// ユーザー更新
exports.updateUser = catchAsync(async (req, res, next) => {
const allowedUpdates = ['name', 'email', 'profile', 'isActive'];
const updates = Object.keys(req.body)
.filter(key => allowedUpdates.includes(key))
.reduce((obj, key) => {
obj[key] = req.body[key];
return obj;
}, {});
const user = await User.findByIdAndUpdate(
req.params.id,
updates,
{
new: true,
runValidators: true
}
);
if (!user) {
return next(new AppError('ユーザーが見つかりません', 404));
}
res.json({
status: 'success',
data: user
});
});
// ユーザー削除
exports.deleteUser = catchAsync(async (req, res, next) => {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return next(new AppError('ユーザーが見つかりません', 404));
}
res.status(204).json({
status: 'success',
data: null
});
});
// 統計情報
exports.getUserStats = catchAsync(async (req, res, next) => {
const stats = await User.aggregate([
{
$match: { isActive: true }
},
{
$group: {
_id: '$role',
count: { $sum: 1 },
avgPostCount: { $avg: { $size: '$posts' } }
}
},
{
$sort: { count: -1 }
}
]);
const monthlySignups = await User.aggregate([
{
$group: {
_id: {
year: { $year: '$createdAt' },
month: { $month: '$createdAt' }
},
count: { $sum: 1 }
}
},
{
$sort: { '_id.year': -1, '_id.month': -1 }
},
{
$limit: 12
}
]);
res.json({
status: 'success',
data: {
roleStats: stats,
monthlySignups
}
});
});
5. 高度なクエリテクニック
// 複雑なアグリゲーション
const getPopularPosts = async () => {
const posts = await Post.aggregate([
// 公開済みの投稿のみ
{ $match: { status: 'published' } },
// 日付でフィルタ(過去30日)
{
$match: {
publishedAt: {
$gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
}
}
},
// いいね数を計算
{
$addFields: {
likeCount: { $size: '$likes' },
commentCount: { $size: '$comments' }
}
},
// 人気度スコアを計算
{
$addFields: {
popularityScore: {
$add: [
{ $multiply: ['$views', 0.1] },
{ $multiply: ['$likeCount', 2] },
{ $multiply: ['$commentCount', 3] }
]
}
}
},
// ソート
{ $sort: { popularityScore: -1 } },
// 上位10件を取得
{ $limit: 10 },
// 著者情報を結合
{
$lookup: {
from: 'users',
localField: 'author',
foreignField: '_id',
as: 'author'
}
},
// 配列を単一オブジェクトに変換
{ $unwind: '$author' },
// 必要なフィールドのみ選択
{
$project: {
title: 1,
slug: 1,
excerpt: 1,
author: {
name: 1,
email: 1,
'profile.avatar': 1
},
category: 1,
tags: 1,
views: 1,
likeCount: 1,
commentCount: 1,
popularityScore: 1,
publishedAt: 1
}
}
]);
return posts;
};
// トランザクションの使用
const transferPost = async (postId, fromUserId, toUserId) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
// 投稿の著者を更新
const post = await Post.findByIdAndUpdate(
postId,
{ author: toUserId },
{ session, new: true }
);
if (!post) {
throw new Error('投稿が見つかりません');
}
// 元の著者から投稿を削除
await User.findByIdAndUpdate(
fromUserId,
{ $pull: { posts: postId } },
{ session }
);
// 新しい著者に投稿を追加
await User.findByIdAndUpdate(
toUserId,
{ $push: { posts: postId } },
{ session }
);
// トランザクションをコミット
await session.commitTransaction();
session.endSession();
return post;
} catch (error) {
// エラーが発生したらロールバック
await session.abortTransaction();
session.endSession();
throw error;
}
};
// バルク操作
const bulkUpdatePosts = async (updates) => {
const bulkOps = updates.map(update => ({
updateOne: {
filter: { _id: update.id },
update: { $set: update.data },
upsert: false
}
}));
const result = await Post.bulkWrite(bulkOps);
return result;
};
PostgreSQL と Sequelize
PostgreSQLは最も高機能なオープンソースのリレーショナルデータベースで、SequelizeはNode.js用の強力なORMです。
1. セットアップ
# PostgreSQLのインストール(macOS)
brew install postgresql
brew services start postgresql
# データベースの作成
createdb myapp_development
createdb myapp_test
createdb myapp_production
# Sequelizeと必要なパッケージのインストール
npm install sequelize pg pg-hstore
npm install --save-dev sequelize-cli
# Sequelizeの初期化
npx sequelize-cli init
2. 設定ファイル
config/config.js
require('dotenv').config();
module.exports = {
development: {
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || null,
database: process.env.DB_NAME || 'myapp_development',
host: process.env.DB_HOST || '127.0.0.1',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: console.log,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
},
test: {
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || null,
database: process.env.DB_NAME_TEST || 'myapp_test',
host: process.env.DB_HOST || '127.0.0.1',
dialect: 'postgres',
logging: false
},
production: {
use_env_variable: 'DATABASE_URL',
dialect: 'postgres',
dialectOptions: {
ssl: {
require: true,
rejectUnauthorized: false
}
},
logging: false,
pool: {
max: 10,
min: 2,
acquire: 30000,
idle: 10000
}
}
};
3. モデルの定義
models/index.js
const { Sequelize } = require('sequelize');
const config = require('../config/config.js')[process.env.NODE_ENV || 'development'];
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
// モデルのインポート
const User = require('./user')(sequelize, Sequelize.DataTypes);
const Product = require('./product')(sequelize, Sequelize.DataTypes);
const Order = require('./order')(sequelize, Sequelize.DataTypes);
const OrderItem = require('./orderItem')(sequelize, Sequelize.DataTypes);
const Category = require('./category')(sequelize, Sequelize.DataTypes);
const Review = require('./review')(sequelize, Sequelize.DataTypes);
// アソシエーションの定義
// User associations
User.hasMany(Order, { foreignKey: 'userId', as: 'orders' });
User.hasMany(Review, { foreignKey: 'userId', as: 'reviews' });
// Product associations
Product.belongsTo(Category, { foreignKey: 'categoryId', as: 'category' });
Product.hasMany(OrderItem, { foreignKey: 'productId', as: 'orderItems' });
Product.hasMany(Review, { foreignKey: 'productId', as: 'reviews' });
// Order associations
Order.belongsTo(User, { foreignKey: 'userId', as: 'user' });
Order.hasMany(OrderItem, { foreignKey: 'orderId', as: 'items' });
// OrderItem associations
OrderItem.belongsTo(Order, { foreignKey: 'orderId' });
OrderItem.belongsTo(Product, { foreignKey: 'productId', as: 'product' });
// Category associations
Category.hasMany(Product, { foreignKey: 'categoryId', as: 'products' });
// Review associations
Review.belongsTo(User, { foreignKey: 'userId', as: 'user' });
Review.belongsTo(Product, { foreignKey: 'productId', as: 'product' });
module.exports = {
sequelize,
Sequelize,
User,
Product,
Order,
OrderItem,
Category,
Review
};
models/user.js
const bcrypt = require('bcryptjs');
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
len: [3, 30],
isAlphanumeric: true
}
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [6, 100]
}
},
firstName: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [1, 50]
}
},
lastName: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [1, 50]
}
},
role: {
type: DataTypes.ENUM('customer', 'admin', 'vendor'),
defaultValue: 'customer'
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
emailVerified: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
emailVerificationToken: DataTypes.STRING,
passwordResetToken: DataTypes.STRING,
passwordResetExpires: DataTypes.DATE,
lastLogin: DataTypes.DATE,
profile: {
type: DataTypes.JSONB,
defaultValue: {},
get() {
return this.getDataValue('profile') || {};
}
},
preferences: {
type: DataTypes.JSONB,
defaultValue: {
notifications: {
email: true,
push: false
},
theme: 'light'
}
},
address: {
type: DataTypes.JSONB,
defaultValue: {}
}
}, {
tableName: 'users',
timestamps: true,
indexes: [
{
fields: ['email']
},
{
fields: ['username']
},
{
fields: ['createdAt']
}
],
hooks: {
beforeCreate: async (user) => {
user.password = await bcrypt.hash(user.password, 10);
},
beforeUpdate: async (user) => {
if (user.changed('password')) {
user.password = await bcrypt.hash(user.password, 10);
}
}
}
});
// インスタンスメソッド
User.prototype.comparePassword = async function(password) {
return bcrypt.compare(password, this.password);
};
User.prototype.getFullName = function() {
return `${this.firstName} ${this.lastName}`;
};
User.prototype.toJSON = function() {
const values = Object.assign({}, this.get());
delete values.password;
delete values.emailVerificationToken;
delete values.passwordResetToken;
delete values.passwordResetExpires;
return values;
};
// クラスメソッド
User.findByEmail = function(email) {
return this.findOne({ where: { email } });
};
User.findActiveUsers = function() {
return this.findAll({
where: { isActive: true },
order: [['createdAt', 'DESC']]
});
};
return User;
};
models/product.js
module.exports = (sequelize, DataTypes) => {
const Product = sequelize.define('Product', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [1, 200]
}
},
slug: {
type: DataTypes.STRING,
unique: true
},
description: {
type: DataTypes.TEXT,
allowNull: false
},
price: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false,
validate: {
min: 0
}
},
compareAtPrice: {
type: DataTypes.DECIMAL(10, 2),
validate: {
min: 0
}
},
cost: {
type: DataTypes.DECIMAL(10, 2),
validate: {
min: 0
}
},
sku: {
type: DataTypes.STRING,
unique: true
},
barcode: DataTypes.STRING,
trackQuantity: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
quantity: {
type: DataTypes.INTEGER,
defaultValue: 0,
validate: {
min: 0
}
},
weight: {
type: DataTypes.DECIMAL(10, 2),
validate: {
min: 0
}
},
weightUnit: {
type: DataTypes.ENUM('kg', 'g', 'lb', 'oz'),
defaultValue: 'kg'
},
status: {
type: DataTypes.ENUM('active', 'draft', 'archived'),
defaultValue: 'active'
},
images: {
type: DataTypes.JSONB,
defaultValue: []
},
tags: {
type: DataTypes.ARRAY(DataTypes.STRING),
defaultValue: []
},
metadata: {
type: DataTypes.JSONB,
defaultValue: {}
},
categoryId: {
type: DataTypes.UUID,
references: {
model: 'categories',
key: 'id'
}
}
}, {
tableName: 'products',
timestamps: true,
paranoid: true,
indexes: [
{
fields: ['slug']
},
{
fields: ['sku']
},
{
fields: ['status']
},
{
fields: ['categoryId']
},
{
type: 'FULLTEXT',
fields: ['name', 'description']
}
],
hooks: {
beforeCreate: (product) => {
if (!product.slug) {
product.slug = product.name
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
}
}
}
});
// スコープ
Product.addScope('active', {
where: { status: 'active' }
});
Product.addScope('inStock', {
where: {
[sequelize.Sequelize.Op.or]: [
{ trackQuantity: false },
{ quantity: { [sequelize.Sequelize.Op.gt]: 0 } }
]
}
});
Product.addScope('withCategory', {
include: [{
model: sequelize.models.Category,
as: 'category'
}]
});
// インスタンスメソッド
Product.prototype.isInStock = function() {
return !this.trackQuantity || this.quantity > 0;
};
Product.prototype.updateStock = async function(quantity, operation = 'decrease') {
if (!this.trackQuantity) return this;
if (operation === 'decrease') {
if (this.quantity < quantity) {
throw new Error('在庫が不足しています');
}
this.quantity -= quantity;
} else {
this.quantity += quantity;
}
return this.save();
};
return Product;
};
4. マイグレーション
migrations/20240101000001-create-users-table.js
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('users', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true,
allowNull: false
},
username: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
email: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
password: {
type: Sequelize.STRING,
allowNull: false
},
firstName: {
type: Sequelize.STRING,
allowNull: false
},
lastName: {
type: Sequelize.STRING,
allowNull: false
},
role: {
type: Sequelize.ENUM('customer', 'admin', 'vendor'),
defaultValue: 'customer'
},
isActive: {
type: Sequelize.BOOLEAN,
defaultValue: true
},
emailVerified: {
type: Sequelize.BOOLEAN,
defaultValue: false
},
emailVerificationToken: Sequelize.STRING,
passwordResetToken: Sequelize.STRING,
passwordResetExpires: Sequelize.DATE,
lastLogin: Sequelize.DATE,
profile: {
type: Sequelize.JSONB,
defaultValue: {}
},
preferences: {
type: Sequelize.JSONB,
defaultValue: {}
},
address: {
type: Sequelize.JSONB,
defaultValue: {}
},
createdAt: {
type: Sequelize.DATE,
allowNull: false
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false
}
});
// インデックスの追加
await queryInterface.addIndex('users', ['email']);
await queryInterface.addIndex('users', ['username']);
await queryInterface.addIndex('users', ['createdAt']);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('users');
}
};
5. 高度なクエリとトランザクション
controllers/productController.js
const { Product, Category, Review, OrderItem, sequelize } = require('../models');
const { Op } = require('sequelize');
// 商品検索(フィルタリング、ソート、ページネーション)
exports.searchProducts = async (req, res) => {
try {
const {
q,
category,
minPrice,
maxPrice,
tags,
inStock,
sortBy = 'createdAt',
order = 'DESC',
page = 1,
limit = 20
} = req.query;
// WHERE句の構築
const where = {};
// テキスト検索
if (q) {
where[Op.or] = [
{ name: { [Op.iLike]: `%${q}%` } },
{ description: { [Op.iLike]: `%${q}%` } }
];
}
// 価格フィルター
if (minPrice || maxPrice) {
where.price = {};
if (minPrice) where.price[Op.gte] = minPrice;
if (maxPrice) where.price[Op.lte] = maxPrice;
}
// タグフィルター
if (tags) {
const tagArray = Array.isArray(tags) ? tags : tags.split(',');
where.tags = { [Op.contains]: tagArray };
}
// 在庫フィルター
if (inStock === 'true') {
where[Op.or] = [
{ trackQuantity: false },
{ quantity: { [Op.gt]: 0 } }
];
}
// ステータスは常にactiveのみ
where.status = 'active';
// カテゴリーフィルター
const include = [{
model: Category,
as: 'category',
where: category ? { slug: category } : undefined,
required: !!category
}];
// クエリ実行
const products = await Product.findAndCountAll({
where,
include,
order: [[sortBy, order]],
limit: parseInt(limit),
offset: (parseInt(page) - 1) * parseInt(limit),
distinct: true,
subQuery: false
});
// レスポンス
res.json({
success: true,
data: products.rows,
pagination: {
total: products.count,
page: parseInt(page),
pages: Math.ceil(products.count / parseInt(limit)),
limit: parseInt(limit)
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// 商品詳細と関連情報の取得
exports.getProductDetail = async (req, res) => {
try {
const product = await Product.findOne({
where: { slug: req.params.slug },
include: [
{
model: Category,
as: 'category',
attributes: ['id', 'name', 'slug']
},
{
model: Review,
as: 'reviews',
attributes: ['id', 'rating', 'comment', 'createdAt'],
include: [{
model: User,
as: 'user',
attributes: ['id', 'firstName', 'lastName']
}],
limit: 10,
order: [['createdAt', 'DESC']]
}
]
});
if (!product) {
return res.status(404).json({
success: false,
error: '商品が見つかりません'
});
}
// 関連商品の取得
const relatedProducts = await Product.findAll({
where: {
categoryId: product.categoryId,
id: { [Op.ne]: product.id },
status: 'active'
},
limit: 4,
order: sequelize.random()
});
// 売上統計
const salesStats = await OrderItem.findOne({
where: { productId: product.id },
attributes: [
[sequelize.fn('COUNT', sequelize.col('id')), 'totalOrders'],
[sequelize.fn('SUM', sequelize.col('quantity')), 'totalSold'],
[sequelize.fn('AVG', sequelize.col('price')), 'avgPrice']
],
raw: true
});
res.json({
success: true,
data: {
product,
relatedProducts,
salesStats
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// 在庫の一括更新(トランザクション使用)
exports.bulkUpdateStock = async (req, res) => {
const t = await sequelize.transaction();
try {
const updates = req.body.updates; // [{ productId, quantity, operation }]
for (const update of updates) {
const product = await Product.findByPk(update.productId, {
transaction: t,
lock: t.LOCK.UPDATE
});
if (!product) {
throw new Error(`商品ID ${update.productId} が見つかりません`);
}
if (update.operation === 'set') {
product.quantity = update.quantity;
} else if (update.operation === 'increase') {
product.quantity += update.quantity;
} else if (update.operation === 'decrease') {
if (product.quantity < update.quantity) {
throw new Error(`商品 ${product.name} の在庫が不足しています`);
}
product.quantity -= update.quantity;
}
await product.save({ transaction: t });
}
await t.commit();
res.json({
success: true,
message: '在庫を更新しました'
});
} catch (error) {
await t.rollback();
res.status(400).json({
success: false,
error: error.message
});
}
};
// 売上レポート(集計クエリ)
exports.getSalesReport = async (req, res) => {
try {
const { startDate, endDate, groupBy = 'day' } = req.query;
// 日付フォーマット
let dateFormat;
switch (groupBy) {
case 'day':
dateFormat = 'YYYY-MM-DD';
break;
case 'week':
dateFormat = 'IYYY-IW';
break;
case 'month':
dateFormat = 'YYYY-MM';
break;
default:
dateFormat = 'YYYY-MM-DD';
}
const salesData = await OrderItem.findAll({
attributes: [
[sequelize.fn('DATE_TRUNC', groupBy, sequelize.col('OrderItem.createdAt')), 'period'],
[sequelize.fn('COUNT', sequelize.col('OrderItem.id')), 'orderCount'],
[sequelize.fn('SUM', sequelize.col('quantity')), 'totalQuantity'],
[sequelize.fn('SUM', sequelize.literal('quantity * price')), 'totalRevenue']
],
include: [{
model: Product,
as: 'product',
attributes: []
}],
where: {
createdAt: {
[Op.between]: [startDate || '2020-01-01', endDate || new Date()]
}
},
group: ['period'],
order: [['period', 'ASC']],
raw: true
});
// カテゴリー別売上
const categoryData = await OrderItem.findAll({
attributes: [
[sequelize.col('product.category.name'), 'categoryName'],
[sequelize.fn('SUM', sequelize.col('quantity')), 'totalQuantity'],
[sequelize.fn('SUM', sequelize.literal('OrderItem.quantity * OrderItem.price')), 'totalRevenue']
],
include: [{
model: Product,
as: 'product',
attributes: [],
include: [{
model: Category,
as: 'category',
attributes: []
}]
}],
where: {
createdAt: {
[Op.between]: [startDate || '2020-01-01', endDate || new Date()]
}
},
group: ['product.category.id', 'product.category.name'],
order: [[sequelize.literal('totalRevenue'), 'DESC']],
raw: true
});
res.json({
success: true,
data: {
salesData,
categoryData
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
Redis でキャッシング
Redisは高速なインメモリデータストアで、キャッシング、セッション管理、リアルタイム機能の実装に最適です。
1. セットアップ
# Redisのインストール(macOS)
brew install redis
brew services start redis
# Node.jsクライアントのインストール
npm install redis ioredis
2. Redis接続の設定
config/redis.js
const Redis = require('ioredis');
// Redis接続の作成
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD,
db: process.env.REDIS_DB || 0,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
maxRetriesPerRequest: 3
});
// 接続イベント
redis.on('connect', () => {
console.log('Redis connected');
});
redis.on('error', (err) => {
console.error('Redis error:', err);
});
// Pub/Sub用の別接続
const pubClient = redis.duplicate();
const subClient = redis.duplicate();
module.exports = {
redis,
pubClient,
subClient
};
3. キャッシングミドルウェア
middleware/cache.js
const { redis } = require('../config/redis');
// キャッシュキーの生成
const generateCacheKey = (req) => {
const { originalUrl, method } = req;
const userId = req.user?.id || 'anonymous';
return `cache:${method}:${originalUrl}:${userId}`;
};
// キャッシュミドルウェア
const cache = (duration = 300) => {
return async (req, res, next) => {
// POSTリクエストはキャッシュしない
if (req.method !== 'GET') {
return next();
}
const key = generateCacheKey(req);
try {
const cachedData = await redis.get(key);
if (cachedData) {
const data = JSON.parse(cachedData);
console.log('Cache hit:', key);
return res.json(data);
}
// キャッシュがない場合は次へ
console.log('Cache miss:', key);
// レスポンスをインターセプト
const originalJson = res.json;
res.json = function(data) {
// レスポンスをキャッシュに保存
redis.setex(key, duration, JSON.stringify(data))
.catch(err => console.error('Cache set error:', err));
// 元のjsonメソッドを呼ぶ
originalJson.call(this, data);
};
next();
} catch (error) {
console.error('Cache middleware error:', error);
next();
}
};
};
// キャッシュの無効化
const invalidateCache = (pattern) => {
return async (req, res, next) => {
try {
const keys = await redis.keys(`cache:*${pattern}*`);
if (keys.length > 0) {
await redis.del(keys);
console.log(`Invalidated ${keys.length} cache keys`);
}
next();
} catch (error) {
console.error('Cache invalidation error:', error);
next();
}
};
};
// タグベースのキャッシュ
class TaggedCache {
constructor(redis) {
this.redis = redis;
}
async set(key, value, tags = [], ttl = 300) {
const pipeline = this.redis.pipeline();
// データを保存
pipeline.setex(key, ttl, JSON.stringify(value));
// タグとキーの関連付け
for (const tag of tags) {
pipeline.sadd(`tag:${tag}`, key);
pipeline.expire(`tag:${tag}`, ttl);
}
await pipeline.exec();
}
async get(key) {
const data = await this.redis.get(key);
return data ? JSON.parse(data) : null;
}
async invalidateTag(tag) {
const keys = await this.redis.smembers(`tag:${tag}`);
if (keys.length > 0) {
const pipeline = this.redis.pipeline();
// キーを削除
for (const key of keys) {
pipeline.del(key);
}
// タグセットを削除
pipeline.del(`tag:${tag}`);
await pipeline.exec();
console.log(`Invalidated ${keys.length} keys for tag: ${tag}`);
}
}
}
const taggedCache = new TaggedCache(redis);
module.exports = {
cache,
invalidateCache,
taggedCache
};
4. セッション管理
config/session.js
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const { redis } = require('./redis');
const sessionConfig = {
store: new RedisStore({
client: redis,
prefix: 'sess:',
}),
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24 * 7, // 1週間
sameSite: 'lax'
},
name: 'sessionId'
};
module.exports = sessionConfig;
5. レート制限
middleware/rateLimiter.js
const { redis } = require('../config/redis');
// スライディングウィンドウ方式のレート制限
const rateLimiter = (options = {}) => {
const {
windowMs = 60000, // 1分
max = 100, // 最大リクエスト数
keyGenerator = (req) => req.ip,
skipSuccessfulRequests = false,
skipFailedRequests = false,
message = 'リクエストが多すぎます。しばらく待ってから再試行してください。'
} = options;
return async (req, res, next) => {
const key = `rate:${keyGenerator(req)}`;
const now = Date.now();
const windowStart = now - windowMs;
try {
const pipeline = redis.pipeline();
// 古いエントリを削除
pipeline.zremrangebyscore(key, '-inf', windowStart);
// 現在のリクエスト数を取得
pipeline.zcard(key);
// 新しいリクエストを追加
pipeline.zadd(key, now, `${now}-${Math.random()}`);
// TTLを設定
pipeline.expire(key, Math.ceil(windowMs / 1000));
const results = await pipeline.exec();
const requestCount = results[1][1];
// レート制限チェック
if (requestCount >= max) {
res.setHeader('X-RateLimit-Limit', max);
res.setHeader('X-RateLimit-Remaining', 0);
res.setHeader('X-RateLimit-Reset', new Date(now + windowMs).toISOString());
return res.status(429).json({
error: message,
retryAfter: Math.ceil(windowMs / 1000)
});
}
// ヘッダーを設定
res.setHeader('X-RateLimit-Limit', max);
res.setHeader('X-RateLimit-Remaining', max - requestCount - 1);
res.setHeader('X-RateLimit-Reset', new Date(now + windowMs).toISOString());
// リクエスト完了後の処理
res.on('finish', () => {
if (
(skipSuccessfulRequests && res.statusCode < 400) ||
(skipFailedRequests && res.statusCode >= 400)
) {
// リクエストをカウントから削除
redis.zrem(key, `${now}-${Math.random()}`);
}
});
next();
} catch (error) {
console.error('Rate limiter error:', error);
next(); // エラーが発生してもリクエストを通す
}
};
};
// API別のレート制限
const apiLimiter = rateLimiter({
windowMs: 15 * 60 * 1000, // 15分
max: 100,
keyGenerator: (req) => `api:${req.ip}:${req.user?.id || 'anonymous'}`
});
// 認証エンドポイントのレート制限
const authLimiter = rateLimiter({
windowMs: 15 * 60 * 1000, // 15分
max: 5,
keyGenerator: (req) => `auth:${req.ip}`,
skipSuccessfulRequests: true
});
module.exports = {
rateLimiter,
apiLimiter,
authLimiter
};
6. リアルタイム機能(Pub/Sub)
services/realtime.js
const { pubClient, subClient } = require('../config/redis');
const EventEmitter = require('events');
class RealtimeService extends EventEmitter {
constructor() {
super();
this.channels = new Map();
this.setupSubscriber();
}
setupSubscriber() {
subClient.on('message', (channel, message) => {
try {
const data = JSON.parse(message);
this.emit(channel, data);
// チャンネル固有のリスナーに通知
const listeners = this.channels.get(channel) || [];
listeners.forEach(listener => listener(data));
} catch (error) {
console.error('Message parse error:', error);
}
});
}
// チャンネルの購読
async subscribe(channel, callback) {
if (!this.channels.has(channel)) {
this.channels.set(channel, []);
await subClient.subscribe(channel);
}
this.channels.get(channel).push(callback);
// アンサブスクライブ関数を返す
return () => {
const listeners = this.channels.get(channel);
const index = listeners.indexOf(callback);
if (index > -1) {
listeners.splice(index, 1);
// リスナーがなくなったらアンサブスクライブ
if (listeners.length === 0) {
this.channels.delete(channel);
subClient.unsubscribe(channel);
}
}
};
}
// メッセージの発行
async publish(channel, data) {
await pubClient.publish(channel, JSON.stringify(data));
}
// ユーザー通知
async notifyUser(userId, notification) {
await this.publish(`user:${userId}`, {
type: 'notification',
data: notification,
timestamp: new Date()
});
}
// ブロードキャスト
async broadcast(event, data) {
await this.publish('broadcast', {
event,
data,
timestamp: new Date()
});
}
}
const realtimeService = new RealtimeService();
// WebSocketとの統合例
const setupWebSocket = (io) => {
io.on('connection', (socket) => {
console.log('Client connected:', socket.id);
// ユーザー固有のチャンネルを購読
socket.on('authenticate', async (userId) => {
socket.userId = userId;
// ユーザーチャンネルを購読
const unsubscribe = await realtimeService.subscribe(
`user:${userId}`,
(data) => {
socket.emit('message', data);
}
);
// 切断時にアンサブスクライブ
socket.on('disconnect', () => {
unsubscribe();
});
});
// ルームへの参加
socket.on('join-room', async (roomId) => {
socket.join(roomId);
const unsubscribe = await realtimeService.subscribe(
`room:${roomId}`,
(data) => {
io.to(roomId).emit('room-message', data);
}
);
socket.on('leave-room', () => {
socket.leave(roomId);
unsubscribe();
});
});
});
};
module.exports = {
realtimeService,
setupWebSocket
};
7. キューシステム
services/queue.js
const Bull = require('bull');
const { redis } = require('../config/redis');
// キューの作成
const emailQueue = new Bull('email', {
redis: {
host: redis.options.host,
port: redis.options.port,
password: redis.options.password
}
});
const imageQueue = new Bull('image-processing', {
redis: {
host: redis.options.host,
port: redis.options.port,
password: redis.options.password
}
});
// メール送信ジョブの処理
emailQueue.process(async (job) => {
const { to, subject, template, data } = job.data;
console.log(`Sending email to ${to}`);
// メール送信ロジック
await sendEmail(to, subject, template, data);
return { sent: true, timestamp: new Date() };
});
// 画像処理ジョブ
imageQueue.process(async (job) => {
const { imagePath, sizes } = job.data;
console.log(`Processing image: ${imagePath}`);
// 画像リサイズロジック
const results = await resizeImage(imagePath, sizes);
return results;
});
// ジョブの追加
const addEmailJob = async (emailData) => {
const job = await emailQueue.add(emailData, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
}
});
return job.id;
};
const addImageJob = async (imageData) => {
const job = await imageQueue.add(imageData, {
priority: imageData.priority || 0,
delay: imageData.delay || 0
});
return job.id;
};
// ジョブイベントの監視
emailQueue.on('completed', (job, result) => {
console.log(`Email job ${job.id} completed:`, result);
});
emailQueue.on('failed', (job, err) => {
console.error(`Email job ${job.id} failed:`, err);
});
module.exports = {
emailQueue,
imageQueue,
addEmailJob,
addImageJob
};
ORMとクエリ最適化
ORMを効率的に使用し、パフォーマンスを最適化するためのテクニックを学びます。
1. N+1問題の解決
// 悪い例:N+1問題が発生
const posts = await Post.findAll();
for (const post of posts) {
post.author = await User.findByPk(post.authorId); // N回のクエリ
}
// 良い例:Eager Loading
const posts = await Post.findAll({
include: [{
model: User,
as: 'author',
attributes: ['id', 'name', 'email'] // 必要なフィールドのみ
}]
});
// さらに最適化:ネストした関連の読み込み
const posts = await Post.findAll({
include: [
{
model: User,
as: 'author',
attributes: ['id', 'name'],
include: [{
model: Profile,
as: 'profile',
attributes: ['avatar', 'bio']
}]
},
{
model: Comment,
as: 'comments',
limit: 5,
order: [['createdAt', 'DESC']],
include: [{
model: User,
as: 'user',
attributes: ['id', 'name']
}]
}
]
});
2. クエリの最適化
// インデックスの活用
// Mongooseの場合
userSchema.index({ email: 1 });
userSchema.index({ createdAt: -1 });
userSchema.index({ 'profile.location': '2dsphere' }); // 地理空間インデックス
// Sequelizeの場合
User.init({
// ... fields
}, {
indexes: [
{
unique: true,
fields: ['email']
},
{
fields: ['createdAt']
},
{
type: 'FULLTEXT',
fields: ['bio']
}
]
});
// 部分インデックス(条件付きインデックス)
Product.init({
// ... fields
}, {
indexes: [
{
fields: ['status', 'categoryId'],
where: {
status: 'active'
}
}
]
});
// 複合インデックスの順序を考慮
// 良い例:より選択的なフィールドを先に
orderSchema.index({ status: 1, userId: 1, createdAt: -1 });
// プロジェクションで必要なフィールドのみ取得
const users = await User.find({}, 'name email createdAt');
// Lean()で軽量なオブジェクトを取得(Mongoose)
const products = await Product
.find({ status: 'active' })
.select('name price')
.lean(); // Mongooseドキュメントではなく、プレーンオブジェクトを返す
3. バッチ処理とストリーミング
// カーソルを使った大量データの処理(MongoDB)
async function processLargeDataset() {
const cursor = User.find({ isActive: true }).cursor();
for (let user = await cursor.next(); user != null; user = await cursor.next()) {
// 各ユーザーを個別に処理
await processUser(user);
}
}
// ストリーミング(PostgreSQL)
const { QueryTypes } = require('sequelize');
const stream = await sequelize.query(
'SELECT * FROM products WHERE status = :status',
{
replacements: { status: 'active' },
type: QueryTypes.SELECT,
raw: true,
stream: true
}
);
stream.on('data', (product) => {
// 各商品を処理
processProduct(product);
});
// バッチ処理
async function batchProcess(model, batchSize = 1000) {
let offset = 0;
let hasMore = true;
while (hasMore) {
const batch = await model.findAll({
limit: batchSize,
offset: offset,
raw: true
});
if (batch.length === 0) {
hasMore = false;
} else {
await processBatch(batch);
offset += batchSize;
}
}
}
4. 接続プールの最適化
// MongoDB接続プールの設定
mongoose.connect(uri, {
maxPoolSize: 10, // 最大接続数
minPoolSize: 2, // 最小接続数
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
});
// PostgreSQL接続プールの設定
const sequelize = new Sequelize({
// ... other config
pool: {
max: 20, // 最大接続数
min: 5, // 最小接続数
acquire: 30000, // 接続取得のタイムアウト
idle: 10000, // アイドル接続のタイムアウト
evict: 1000, // アイドル接続チェックの間隔
validate: (client) => {
// 接続の有効性をチェック
return client.query('SELECT 1').then(() => true).catch(() => false);
}
}
});
// 接続プールの監視
sequelize.connectionManager.pool.on('acquire', () => {
console.log('Connection acquired');
});
sequelize.connectionManager.pool.on('release', () => {
console.log('Connection released');
});
5. クエリビルダーパターン
// 柔軟なクエリビルダー
class QueryBuilder {
constructor(model) {
this.model = model;
this.query = {};
this.options = {
limit: 20,
offset: 0,
order: [['createdAt', 'DESC']]
};
}
where(conditions) {
Object.assign(this.query, conditions);
return this;
}
whereIn(field, values) {
this.query[field] = { [Op.in]: values };
return this;
}
whereBetween(field, min, max) {
this.query[field] = { [Op.between]: [min, max] };
return this;
}
whereText(fields, searchTerm) {
this.query[Op.or] = fields.map(field => ({
[field]: { [Op.iLike]: `%${searchTerm}%` }
}));
return this;
}
include(association) {
if (!this.options.include) {
this.options.include = [];
}
this.options.include.push(association);
return this;
}
orderBy(field, direction = 'ASC') {
this.options.order = [[field, direction]];
return this;
}
limit(limit) {
this.options.limit = limit;
return this;
}
offset(offset) {
this.options.offset = offset;
return this;
}
paginate(page, perPage = 20) {
this.options.limit = perPage;
this.options.offset = (page - 1) * perPage;
return this;
}
async execute() {
const results = await this.model.findAndCountAll({
where: this.query,
...this.options
});
return {
data: results.rows,
total: results.count,
page: Math.floor(this.options.offset / this.options.limit) + 1,
pages: Math.ceil(results.count / this.options.limit)
};
}
}
// 使用例
const results = await new QueryBuilder(Product)
.where({ status: 'active' })
.whereBetween('price', 100, 1000)
.whereText(['name', 'description'], 'laptop')
.include({
model: Category,
as: 'category'
})
.orderBy('price', 'DESC')
.paginate(1, 20)
.execute();
データベース設計
効率的で拡張可能なデータベース設計の原則とパターンを学びます。
1. 正規化と非正規化
// 正規化されたスキーマ(リレーショナル)
// 第3正規形まで正規化
const User = sequelize.define('User', {
id: { type: DataTypes.UUID, primaryKey: true },
email: { type: DataTypes.STRING, unique: true },
username: { type: DataTypes.STRING, unique: true }
});
const UserProfile = sequelize.define('UserProfile', {
userId: { type: DataTypes.UUID, references: { model: User } },
firstName: DataTypes.STRING,
lastName: DataTypes.STRING,
bio: DataTypes.TEXT,
avatar: DataTypes.STRING
});
const Address = sequelize.define('Address', {
id: { type: DataTypes.UUID, primaryKey: true },
userId: { type: DataTypes.UUID, references: { model: User } },
type: DataTypes.ENUM('home', 'work', 'other'),
street: DataTypes.STRING,
city: DataTypes.STRING,
state: DataTypes.STRING,
country: DataTypes.STRING,
zipCode: DataTypes.STRING
});
// 非正規化されたスキーマ(NoSQL)
// パフォーマンスを優先して一部のデータを埋め込み
const userSchema = new Schema({
email: String,
username: String,
profile: {
firstName: String,
lastName: String,
bio: String,
avatar: String
},
addresses: [{
type: { type: String, enum: ['home', 'work', 'other'] },
street: String,
city: String,
state: String,
country: String,
zipCode: String
}],
// 頻繁にアクセスされる集計データを保存
stats: {
postCount: { type: Number, default: 0 },
followerCount: { type: Number, default: 0 },
followingCount: { type: Number, default: 0 }
}
});
2. スキーマ設計パターン
// 1. ポリモーフィック関連
const Comment = sequelize.define('Comment', {
id: { type: DataTypes.UUID, primaryKey: true },
content: DataTypes.TEXT,
commentableId: DataTypes.UUID,
commentableType: DataTypes.STRING, // 'Post', 'Product', 'User'
userId: DataTypes.UUID
});
// 使用例
Comment.belongsTo(Post, {
foreignKey: 'commentableId',
constraints: false,
as: 'post'
});
Comment.belongsTo(Product, {
foreignKey: 'commentableId',
constraints: false,
as: 'product'
});
// 2. 階層データ(ツリー構造)
const Category = sequelize.define('Category', {
id: { type: DataTypes.UUID, primaryKey: true },
name: DataTypes.STRING,
parentId: {
type: DataTypes.UUID,
references: { model: 'categories', key: 'id' }
},
path: DataTypes.STRING, // '/electronics/computers/laptops'
depth: DataTypes.INTEGER
});
// Materialized Path Pattern
Category.beforeCreate((category) => {
if (category.parentId) {
const parent = await Category.findByPk(category.parentId);
category.path = `${parent.path}/${category.name}`;
category.depth = parent.depth + 1;
} else {
category.path = `/${category.name}`;
category.depth = 0;
}
});
// 3. タグシステム(多対多)
const Tag = sequelize.define('Tag', {
id: { type: DataTypes.UUID, primaryKey: true },
name: { type: DataTypes.STRING, unique: true },
slug: { type: DataTypes.STRING, unique: true }
});
const PostTag = sequelize.define('PostTag', {
postId: DataTypes.UUID,
tagId: DataTypes.UUID,
order: DataTypes.INTEGER
});
Post.belongsToMany(Tag, { through: PostTag });
Tag.belongsToMany(Post, { through: PostTag });
// 4. イベントソーシング
const Event = sequelize.define('Event', {
id: { type: DataTypes.UUID, primaryKey: true },
aggregateId: DataTypes.UUID,
aggregateType: DataTypes.STRING,
eventType: DataTypes.STRING,
eventData: DataTypes.JSONB,
userId: DataTypes.UUID,
version: DataTypes.INTEGER,
timestamp: DataTypes.DATE
}, {
timestamps: false,
indexes: [
{ fields: ['aggregateId', 'version'] },
{ fields: ['eventType'] },
{ fields: ['timestamp'] }
]
});
3. 時系列データの設計
// 時系列データ用のスキーマ
const Metric = sequelize.define('Metric', {
id: { type: DataTypes.BIGINT, autoIncrement: true, primaryKey: true },
deviceId: DataTypes.STRING,
metricType: DataTypes.STRING,
value: DataTypes.FLOAT,
timestamp: DataTypes.DATE,
metadata: DataTypes.JSONB
}, {
tableName: 'metrics',
timestamps: false,
indexes: [
{
fields: ['deviceId', 'timestamp']
},
{
fields: ['metricType', 'timestamp']
}
]
});
// パーティショニング(PostgreSQL)
await sequelize.query(`
CREATE TABLE metrics_2024_01 PARTITION OF metrics
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
`);
// 集計用のマテリアライズドビュー
await sequelize.query(`
CREATE MATERIALIZED VIEW hourly_metrics AS
SELECT
deviceId,
metricType,
DATE_TRUNC('hour', timestamp) as hour,
AVG(value) as avg_value,
MIN(value) as min_value,
MAX(value) as max_value,
COUNT(*) as count
FROM metrics
GROUP BY deviceId, metricType, DATE_TRUNC('hour', timestamp);
CREATE INDEX ON hourly_metrics (deviceId, hour);
`);
4. マルチテナンシー
// スキーマレベルの分離
class TenantConnection {
constructor() {
this.connections = new Map();
}
async getConnection(tenantId) {
if (!this.connections.has(tenantId)) {
const config = {
...baseConfig,
database: `tenant_${tenantId}`
};
const connection = new Sequelize(config);
await connection.authenticate();
this.connections.set(tenantId, connection);
}
return this.connections.get(tenantId);
}
}
// 行レベルの分離
const BaseModel = sequelize.define('BaseModel', {
tenantId: {
type: DataTypes.UUID,
allowNull: false
}
}, {
hooks: {
beforeFind: (options) => {
// 自動的にtenantIdフィルターを追加
if (options.where && !options.where.tenantId) {
options.where.tenantId = getCurrentTenantId();
}
}
}
});
// スコープを使った実装
Product.addScope('tenant', (tenantId) => ({
where: { tenantId }
}));
// 使用
const products = await Product.scope({ method: ['tenant', tenantId] }).findAll();
マイグレーション
データベースのスキーマ変更を安全に管理する方法を学びます。
1. マイグレーションの基本
# Sequelizeマイグレーションの作成
npx sequelize-cli migration:generate --name add-user-table
# マイグレーションの実行
npx sequelize-cli db:migrate
# マイグレーションのロールバック
npx sequelize-cli db:migrate:undo
# 特定のマイグレーションまでロールバック
npx sequelize-cli db:migrate:undo:all --to 20240101000000-create-user.js
2. 安全なマイグレーション戦略
// カラムの追加(安全)
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'phoneNumber', {
type: Sequelize.STRING,
allowNull: true, // 最初はNULL許可
defaultValue: null
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('users', 'phoneNumber');
}
};
// カラムの削除(段階的に実行)
// Step 1: アプリケーションコードから使用を削除
// Step 2: カラムをNULL許可に変更
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.changeColumn('users', 'oldColumn', {
type: Sequelize.STRING,
allowNull: true
});
}
};
// Step 3: 実際にカラムを削除
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('users', 'oldColumn');
}
};
// インデックスの追加(CONCURRENTLYを使用)
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query(
'CREATE INDEX CONCURRENTLY idx_users_email ON users(email);'
);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query(
'DROP INDEX CONCURRENTLY idx_users_email;'
);
}
};
3. データマイグレーション
// データの変換を伴うマイグレーション
module.exports = {
up: async (queryInterface, Sequelize) => {
// 新しいカラムを追加
await queryInterface.addColumn('users', 'fullName', {
type: Sequelize.STRING
});
// バッチでデータを更新
const batchSize = 1000;
let offset = 0;
while (true) {
const users = await queryInterface.sequelize.query(
`SELECT id, "firstName", "lastName" FROM users
LIMIT :limit OFFSET :offset`,
{
replacements: { limit: batchSize, offset },
type: Sequelize.QueryTypes.SELECT
}
);
if (users.length === 0) break;
// バルクアップデート
const updates = users.map(user => ({
id: user.id,
fullName: `${user.firstName} ${user.lastName}`
}));
await queryInterface.bulkUpdate('users', updates);
offset += batchSize;
console.log(`Updated ${offset} users...`);
}
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('users', 'fullName');
}
};
4. MongoDBマイグレーション
// migrate-mongo の使用
const migration = {
async up(db, client) {
// コレクションの作成
await db.createCollection('products');
// インデックスの作成
await db.collection('products').createIndex({ sku: 1 }, { unique: true });
await db.collection('products').createIndex({ name: 'text', description: 'text' });
// 既存データの更新
await db.collection('users').updateMany(
{ role: { $exists: false } },
{ $set: { role: 'customer' } }
);
// フィールドのリネーム
await db.collection('orders').updateMany(
{},
{ $rename: { 'total': 'totalAmount' } }
);
},
async down(db, client) {
await db.collection('products').drop();
await db.collection('orders').updateMany(
{},
{ $rename: { 'totalAmount': 'total' } }
);
}
};
module.exports = migration;
バックアップとレプリケーション
データの安全性を確保するためのバックアップ戦略とレプリケーションの設定方法を学びます。
1. PostgreSQLバックアップ
# 論理バックアップ(pg_dump)
pg_dump -h localhost -U postgres -d myapp > backup.sql
# カスタムフォーマットでバックアップ(圧縮)
pg_dump -h localhost -U postgres -d myapp -Fc > backup.dump
# 特定のテーブルのみバックアップ
pg_dump -h localhost -U postgres -d myapp -t users -t orders > tables_backup.sql
# バックアップのリストア
psql -h localhost -U postgres -d myapp_restore < backup.sql
# カスタムフォーマットのリストア
pg_restore -h localhost -U postgres -d myapp_restore backup.dump
2. MongoDBバックアップ
# mongodumpを使用したバックアップ
mongodump --host localhost --port 27017 --db myapp --out /backup/mongo/
# 特定のコレクションのバックアップ
mongodump --host localhost --port 27017 --db myapp --collection users
# 圧縮バックアップ
mongodump --archive=myapp.archive --gzip --db myapp
# リストア
mongorestore --host localhost --port 27017 /backup/mongo/myapp/
# アーカイブからのリストア
mongorestore --archive=myapp.archive --gzip
3. 自動バックアップスクリプト
scripts/backup.js
const { spawn } = require('child_process');
const fs = require('fs').promises;
const path = require('path');
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
class BackupManager {
constructor(config) {
this.config = config;
this.backupDir = config.backupDir || '/tmp/backups';
}
async backupPostgres() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `postgres-backup-${timestamp}.dump`;
const filepath = path.join(this.backupDir, filename);
// バックアップディレクトリの作成
await fs.mkdir(this.backupDir, { recursive: true });
// pg_dumpの実行
return new Promise((resolve, reject) => {
const pgDump = spawn('pg_dump', [
'-h', this.config.postgres.host,
'-U', this.config.postgres.user,
'-d', this.config.postgres.database,
'-Fc',
'-f', filepath
], {
env: {
...process.env,
PGPASSWORD: this.config.postgres.password
}
});
pgDump.on('exit', (code) => {
if (code === 0) {
resolve(filepath);
} else {
reject(new Error(`pg_dump exited with code ${code}`));
}
});
});
}
async backupMongoDB() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `mongodb-backup-${timestamp}.archive`;
const filepath = path.join(this.backupDir, filename);
return new Promise((resolve, reject) => {
const mongoDump = spawn('mongodump', [
'--uri', this.config.mongodb.uri,
'--archive=' + filepath,
'--gzip'
]);
mongoDump.on('exit', (code) => {
if (code === 0) {
resolve(filepath);
} else {
reject(new Error(`mongodump exited with code ${code}`));
}
});
});
}
async uploadToS3(filepath) {
const fileStream = await fs.readFile(filepath);
const filename = path.basename(filepath);
const params = {
Bucket: this.config.s3.bucket,
Key: `backups/${filename}`,
Body: fileStream,
ServerSideEncryption: 'AES256'
};
const result = await s3.upload(params).promise();
console.log(`Uploaded to S3: ${result.Location}`);
// ローカルファイルを削除
await fs.unlink(filepath);
return result.Location;
}
async runBackup() {
try {
console.log('Starting backup process...');
// PostgreSQLバックアップ
if (this.config.postgres) {
const pgFile = await this.backupPostgres();
await this.uploadToS3(pgFile);
console.log('PostgreSQL backup completed');
}
// MongoDBバックアップ
if (this.config.mongodb) {
const mongoFile = await this.backupMongoDB();
await this.uploadToS3(mongoFile);
console.log('MongoDB backup completed');
}
// 古いバックアップの削除
await this.cleanupOldBackups();
console.log('Backup process completed successfully');
} catch (error) {
console.error('Backup failed:', error);
// 通知を送信
await this.sendNotification('Backup failed', error.message);
}
}
async cleanupOldBackups() {
const retentionDays = this.config.retentionDays || 30;
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
const params = {
Bucket: this.config.s3.bucket,
Prefix: 'backups/'
};
const objects = await s3.listObjectsV2(params).promise();
const toDelete = objects.Contents
.filter(obj => new Date(obj.LastModified) < cutoffDate)
.map(obj => ({ Key: obj.Key }));
if (toDelete.length > 0) {
await s3.deleteObjects({
Bucket: this.config.s3.bucket,
Delete: { Objects: toDelete }
}).promise();
console.log(`Deleted ${toDelete.length} old backups`);
}
}
}
// Cronジョブとして実行
const cron = require('node-cron');
const backupManager = new BackupManager(require('./backup-config'));
// 毎日午前2時に実行
cron.schedule('0 2 * * *', async () => {
await backupManager.runBackup();
});
4. レプリケーション設定
MongoDB レプリカセット
// レプリカセット接続
mongoose.connect('mongodb://localhost:27017,localhost:27018,localhost:27019/myapp', {
replicaSet: 'rs0',
readPreference: 'secondaryPreferred', // 読み取り負荷を分散
w: 'majority', // 書き込み確認
j: true, // ジャーナル書き込み
wtimeout: 5000
});
// 読み取り設定の制御
const User = mongoose.model('User', userSchema);
// プライマリから読み取り(最新データが必要な場合)
const user = await User.findById(userId).read('primary');
// セカンダリから読み取り(負荷分散)
const users = await User.find({}).read('secondary');
// 最寄りのノードから読み取り
const products = await Product.find({}).read('nearest');
PostgreSQL ストリーミングレプリケーション
# プライマリサーバーの設定(postgresql.conf)
wal_level = replica
max_wal_senders = 3
wal_keep_segments = 64
archive_mode = on
archive_command = 'cp %p /var/lib/postgresql/archive/%f'
# スタンバイサーバーの設定
# 1. ベースバックアップの取得
pg_basebackup -h primary_host -D /var/lib/postgresql/data -U replicator -P -W
# 2. recovery.conf の作成
standby_mode = 'on'
primary_conninfo = 'host=primary_host port=5432 user=replicator'
trigger_file = '/tmp/postgresql.trigger'
5. 災害復旧計画
// 復旧手順の自動化
class DisasterRecovery {
async performHealthCheck() {
const checks = {
postgres: await this.checkPostgres(),
mongodb: await this.checkMongoDB(),
redis: await this.checkRedis()
};
return checks;
}
async checkPostgres() {
try {
await sequelize.authenticate();
const [result] = await sequelize.query('SELECT 1');
return { status: 'healthy', latency: result.time };
} catch (error) {
return { status: 'unhealthy', error: error.message };
}
}
async initiateFailover() {
console.log('Initiating failover process...');
// 1. プライマリの停止を確認
const primaryHealth = await this.checkPrimary();
if (primaryHealth.status === 'healthy') {
throw new Error('Primary is still healthy, aborting failover');
}
// 2. スタンバイをプライマリに昇格
await this.promoteStandby();
// 3. アプリケーションの接続先を更新
await this.updateConnectionStrings();
// 4. 通知を送信
await this.notifyTeam('Failover completed', {
oldPrimary: this.config.primary,
newPrimary: this.config.standby
});
}
async performBackupRestore(backupId) {
// 1. バックアップのダウンロード
const backupPath = await this.downloadBackup(backupId);
// 2. データベースの復元
await this.restoreDatabase(backupPath);
// 3. 整合性チェック
await this.verifyDataIntegrity();
// 4. アプリケーションの再起動
await this.restartApplication();
}
}
パフォーマンスチューニング
データベースのパフォーマンスを最大化するための実践的なテクニックを学びます。
1. クエリの分析と最適化
// PostgreSQL EXPLAIN分析
const analyzeQuery = async (query) => {
const result = await sequelize.query(
`EXPLAIN (ANALYZE, BUFFERS) ${query}`,
{ type: sequelize.QueryTypes.SELECT }
);
console.log('Query Plan:', result);
};
// スロークエリログの有効化
sequelize.options.logging = (sql, timing) => {
if (timing > 1000) { // 1秒以上かかるクエリ
console.warn('Slow query detected:', {
sql,
duration: timing,
timestamp: new Date()
});
// スロークエリをRedisに記録
redis.zadd('slow_queries', Date.now(), JSON.stringify({
sql,
duration: timing,
timestamp: new Date()
}));
}
};
// MongoDB プロファイリング
db.setProfilingLevel(1, { slowms: 100 }); // 100ms以上のクエリを記録
// プロファイル結果の分析
const slowQueries = await db.collection('system.profile')
.find({ millis: { $gt: 100 } })
.sort({ ts: -1 })
.limit(10)
.toArray();
2. インデックスの最適化
// インデックス使用状況の分析
async function analyzeIndexUsage() {
// PostgreSQL
const pgIndexStats = await sequelize.query(`
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch,
pg_size_pretty(pg_relation_size(indexrelid)) as index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
`, { type: sequelize.QueryTypes.SELECT });
console.log('Unused indexes:', pgIndexStats);
// MongoDB
const collections = await db.listCollections().toArray();
for (const collection of collections) {
const stats = await db.collection(collection.name).aggregate([
{ $indexStats: {} }
]).toArray();
const unusedIndexes = stats.filter(idx =>
idx.accesses.ops === 0 && idx.name !== '_id_'
);
if (unusedIndexes.length > 0) {
console.log(`Unused indexes in ${collection.name}:`, unusedIndexes);
}
}
}
// 複合インデックスの最適化
// カーディナリティの高い順に並べる
userSchema.index({ status: 1, createdAt: -1, userId: 1 }); // 悪い例
userSchema.index({ userId: 1, createdAt: -1, status: 1 }); // 良い例
// 部分インデックス(条件付きインデックス)
await sequelize.query(`
CREATE INDEX idx_active_products
ON products(name, price)
WHERE status = 'active';
`);
3. 接続プールとリソース管理
// 接続プールの監視と調整
class ConnectionPoolMonitor {
constructor(sequelize, redis) {
this.sequelize = sequelize;
this.redis = redis;
this.metrics = {
acquired: 0,
released: 0,
pending: 0,
failed: 0
};
}
start() {
const pool = this.sequelize.connectionManager.pool;
pool.on('acquire', () => {
this.metrics.acquired++;
this.metrics.pending = pool.pending;
this.logMetrics();
});
pool.on('release', () => {
this.metrics.released++;
this.logMetrics();
});
pool.on('error', () => {
this.metrics.failed++;
this.logMetrics();
});
// 定期的にメトリクスを記録
setInterval(() => {
this.saveMetrics();
}, 60000); // 1分ごと
}
logMetrics() {
const pool = this.sequelize.connectionManager.pool;
console.log('Connection Pool Stats:', {
size: pool.size,
available: pool.available,
using: pool.using,
pending: pool.pending,
...this.metrics
});
}
async saveMetrics() {
const timestamp = Date.now();
const pool = this.sequelize.connectionManager.pool;
await this.redis.zadd('pool_metrics', timestamp, JSON.stringify({
timestamp,
size: pool.size,
available: pool.available,
using: pool.using,
pending: pool.pending,
...this.metrics
}));
}
async getPoolRecommendations() {
const recentMetrics = await this.redis.zrange(
'pool_metrics',
-100,
-1
);
const parsed = recentMetrics.map(m => JSON.parse(m));
const avgPending = parsed.reduce((sum, m) => sum + m.pending, 0) / parsed.length;
const maxUsing = Math.max(...parsed.map(m => m.using));
const recommendations = [];
if (avgPending > 5) {
recommendations.push('接続プールサイズを増やすことを検討してください');
}
if (maxUsing < pool.max * 0.5) {
recommendations.push('接続プールサイズを減らすことができます');
}
return recommendations;
}
}
4. キャッシュ戦略の最適化
// 多層キャッシュ戦略
class MultiLayerCache {
constructor(redis) {
this.redis = redis;
this.localCache = new Map();
this.stats = {
localHits: 0,
redisHits: 0,
misses: 0
};
}
async get(key) {
// L1: ローカルメモリキャッシュ
if (this.localCache.has(key)) {
this.stats.localHits++;
return this.localCache.get(key).value;
}
// L2: Redisキャッシュ
const redisValue = await this.redis.get(key);
if (redisValue) {
this.stats.redisHits++;
const parsed = JSON.parse(redisValue);
// ローカルキャッシュに保存
this.setLocal(key, parsed, 60); // 1分間
return parsed;
}
this.stats.misses++;
return null;
}
async set(key, value, ttl = 300) {
// 両方のキャッシュに保存
await this.redis.setex(key, ttl, JSON.stringify(value));
this.setLocal(key, value, Math.min(ttl, 300)); // 最大5分
}
setLocal(key, value, ttl) {
this.localCache.set(key, {
value,
expires: Date.now() + (ttl * 1000)
});
// メモリ制限(1000エントリ)
if (this.localCache.size > 1000) {
const firstKey = this.localCache.keys().next().value;
this.localCache.delete(firstKey);
}
}
// 期限切れエントリの削除
cleanup() {
const now = Date.now();
for (const [key, entry] of this.localCache.entries()) {
if (entry.expires < now) {
this.localCache.delete(key);
}
}
}
getStats() {
const total = this.stats.localHits + this.stats.redisHits + this.stats.misses;
return {
...this.stats,
hitRate: total > 0 ? (this.stats.localHits + this.stats.redisHits) / total : 0,
localHitRate: total > 0 ? this.stats.localHits / total : 0
};
}
}
// ウォームアップ戦略
class CacheWarmer {
constructor(cache, models) {
this.cache = cache;
this.models = models;
}
async warmupPopularData() {
console.log('Starting cache warmup...');
// 人気商品をキャッシュ
const popularProducts = await this.models.Product.findAll({
where: { status: 'active' },
order: [['views', 'DESC']],
limit: 100
});
for (const product of popularProducts) {
await this.cache.set(
`product:${product.id}`,
product.toJSON(),
3600 // 1時間
);
}
// カテゴリーをキャッシュ
const categories = await this.models.Category.findAll({
include: [{
model: this.models.Product,
as: 'products',
where: { status: 'active' },
required: false,
attributes: ['id']
}]
});
for (const category of categories) {
await this.cache.set(
`category:${category.id}`,
category.toJSON(),
7200 // 2時間
);
}
console.log('Cache warmup completed');
}
}
5. データベース設定の最適化
-- PostgreSQL パフォーマンス設定
-- postgresql.conf
-- メモリ設定
shared_buffers = '4GB' -- 総メモリの25%程度
effective_cache_size = '12GB' -- 総メモリの75%程度
work_mem = '64MB' -- ソート/ハッシュ操作用
maintenance_work_mem = '512MB' -- VACUUM, CREATE INDEX用
-- チェックポイント設定
checkpoint_timeout = '15min'
checkpoint_completion_target = 0.9
wal_buffers = '16MB'
-- 並列処理
max_parallel_workers_per_gather = 4
max_parallel_workers = 8
-- 統計情報
default_statistics_target = 100
random_page_cost = 1.1 -- SSDの場合
// MongoDB パフォーマンス設定
// mongod.conf
{
"systemLog": {
"destination": "file",
"path": "/var/log/mongodb/mongod.log",
"logAppend": true
},
"storage": {
"dbPath": "/var/lib/mongodb",
"journal": {
"enabled": true
},
"wiredTiger": {
"engineConfig": {
"cacheSizeGB": 4,
"journalCompressor": "snappy"
},
"collectionConfig": {
"blockCompressor": "zstd"
}
}
},
"net": {
"bindIp": "0.0.0.0",
"maxIncomingConnections": 1000
},
"operationProfiling": {
"mode": "slowOp",
"slowOpThresholdMs": 100
}
}
ベストプラクティス
データベース開発における重要な原則とベストプラクティスをまとめます。
1. セキュリティ
- SQLインジェクション対策(パラメータ化クエリの使用)
- 最小権限の原則に従ったユーザー権限設定
- 接続の暗号化(SSL/TLS)
- 機密データの暗号化
- 定期的なセキュリティ監査
2. パフォーマンス
- 適切なインデックスの設計と管理
- クエリの最適化とEXPLAIN分析
- 接続プールの適切な設定
- キャッシュ戦略の実装
- 定期的なパフォーマンス監視
3. 可用性
- レプリケーションの設定
- 自動フェイルオーバーの実装
- 定期的なバックアップとテスト
- 災害復旧計画の策定
- ヘルスチェックの実装
4. 保守性
- 明確な命名規則の採用
- スキーマのバージョン管理
- 包括的なドキュメンテーション
- テスト環境の整備
- 監視とアラートの設定
まとめ
このチュートリアルでは、Node.jsアプリケーションにおける主要なデータベース技術(MongoDB、PostgreSQL、Redis)の実践的な使用方法を学びました。適切なデータベースの選択、効率的な設計、パフォーマンスの最適化、そして信頼性の高い運用方法を理解することで、スケーラブルで堅牢なアプリケーションを構築できるようになります。