dotnet-postgresql-best-practices について
PostgreSQLをEntity Framework Coreと使う.NET開発者向けの実践ガイドです。命名規則、データ型、インデックス、制約、性能、保守、セキュリティを扱います。
主な機能
- 01snake_case命名とEF Core設定
- 02PostgreSQLの型と主キーの選択
- 03インデックス戦略と制約設計
- 04クエリとバッチ処理の最適化
- 05データベース保守とセキュリティの指針
活用シーン
- 01PostgreSQLのスキーマと列型の計画
- 02一般的なクエリ向けのインデックス設計
- 03Entity Framework Coreの読み書きの最適化
- 04データベース保守と接続セキュリティの確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ ronnythedev/ dotnet-clean-architecture-skills --skill dotnet-postgresql-best-practicesインストール後、Claude Code で/dotnet-postgresql-best-practicesを入力して読み込まれたか確認します。
詳細
.NET 向け PostgreSQL のベストプラクティス
概要
.NET と Entity Framework Core を使用する際の、PostgreSQL データベース設計、命名規則、インデックス作成、パフォーマンス最適化に関するベストプラクティスです。
クイックリファレンス
| カテゴリ | ベストプラクティス |
|---|---|
| 命名 | テーブルと列には snake_case を使用 |
| 主キー | uuid(Guid)または bigserial を使用 |
| タイムスタンプ | UTC の timestamptz を使用 |
| インデックス | 外部キーと一意制約にインデックスを作成 |
| テキスト | 長さ制限が必要な場合を除き、varchar ではなく text を使用 |
| JSON | json ではなく jsonb を使用 |
命名規則
スネークケース標準
PostgreSQL では、すべての識別子に snake_case を使用するのが慣例です。
-- ✅ CORRECT: Snake case
CREATE TABLE user_profiles (
user_id uuid PRIMARY KEY,
first_name text NOT NULL,
last_name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
updated_at timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
);
-- ❌ WRONG: PascalCase or camelCase
CREATE TABLE UserProfiles (
UserId uuid PRIMARY KEY,
firstName text NOT NULL
);
EF Core のスネークケース設定
// DependencyInjection.cs
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseNpgsql(connectionString)
.UseSnakeCaseNamingConvention(); // Converts C# PascalCase to snake_case
});
# Install package
dotnet add package EFCore.NamingConventions
命名パターン
| オブジェクト | パターン | 例 |
|---|---|---|
| テーブル | snake_case(複数形) | user_profiles、order_items |
| 列 | snake_case | first_name、created_at |
| 主キー | pk_{table} | pk_users、pk_orders |
| 外部キー | fk_{table}_{ref_table} | fk_orders_users |
| インデックス | ix_{table}_{column(s)} | ix_users_email |
| 一意インデックス | uix_{table}_{column(s)} | uix_users_email |
| チェック制約 | ck_{table}_{column} | ck_users_age |
| シーケンス | seq_{table}_{column} | seq_orders_order_number |
データ型
推奨する型
| C# 型 | PostgreSQL 型 | 備考 |
|---|---|---|
Guid | uuid | 主キーに推奨 |
string(上限なし) | text | varchar より柔軟 |
string(上限あり) | varchar(n) | 長さ制限が必要な場合のみ |
int | integer | 4 バイト、-2B ~ +2B |
long | bigint | 8 バイト |
decimal | numeric(p,s) | 正確な精度 |
double | double precision | 浮動小数点数 |
bool | boolean | true/false/null |
DateTime | timestamptz | 常にタイムゾーン付きで使用 |
byte[] | bytea | バイナリデータ |
Dictionary<string,object> | jsonb | 構造化データ |
string[] | text[] | 配列型 |
Text と Varchar の比較
-- ✅ PREFERRED: Use text for most string columns
CREATE TABLE products (
id uuid PRIMARY KEY,
name text NOT NULL,
description text
);
-- ⚠️ USE SPARINGLY: Only when you need to enforce length
CREATE TABLE users (
id uuid PRIMARY KEY,
email varchar(255) NOT NULL -- Email has practical length limit
);
text を使う理由
- PostgreSQL ではパフォーマンスに差がありません
- より柔軟です(任意の長さ制限がありません)
- 変更が容易です(長さの変更にマイグレーションは不要です)
タイムスタンプ
-- ✅ CORRECT: timestamptz with UTC default
created_at timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
updated_at timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
-- ❌ WRONG: timestamp without time zone
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
// EF Core configuration
builder.Property(e => e.CreatedAt)
.HasColumnType("timestamptz")
.IsRequired()
.HasDefaultValueSql("CURRENT_TIMESTAMP AT TIME ZONE 'UTC'");
柔軟なデータには JSONB を使用
-- ✅ Use jsonb (binary JSON, faster, indexable)
metadata jsonb
-- ❌ Don't use json (text-based, slower)
metadata json
// EF Core configuration
builder.Property(e => e.Metadata)
.HasColumnType("jsonb");
// Or for owned types (EF Core 7+)
builder.OwnsOne(e => e.Settings, settingsBuilder =>
{
settingsBuilder.ToJson(); // Stores as jsonb
});
主キー
UUID(Guid)— 推奨
-- ✅ RECOMMENDED: UUID primary keys
CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email text NOT NULL
);
// EF Core - App generates GUIDs
builder.Property(e => e.Id)
.ValueGeneratedNever(); // Don't let DB generate
// In domain entity
public static User Create(...)
{
return new User(Guid.NewGuid(), ...); // App generates
}
メリット:
- グローバルに一意です(データベース間で衝突しません)
- クライアント側で生成できます
- 分散システムに適しています
- 連番から値を推測されるセキュリティリスクがありません
Serial/BigSerial の代替案
-- Alternative: Auto-increment
CREATE TABLE orders (
id bigserial PRIMARY KEY,
order_number text NOT NULL
);
// EF Core
builder.Property(e => e.Id)
.UseIdentityColumn(); // PostgreSQL IDENTITY
インデックス戦略
外部キーにインデックスを作成
-- ✅ ALWAYS index foreign keys
CREATE INDEX ix_orders_user_id ON orders(user_id);
CREATE INDEX ix_order_items_order_id ON order_items(order_id);
EF Core が自動的に作成しますが、確認してください。
builder.HasIndex(o => o.UserId);
一意インデックス
-- ✅ Unique constraints
CREATE UNIQUE INDEX uix_users_email ON users(email);
CREATE UNIQUE INDEX uix_users_username ON users(username);
builder.HasIndex(u => u.Email).IsUnique();
複合インデックス
-- ✅ Composite indexes for common query patterns
CREATE INDEX ix_orders_user_status ON orders(user_id, status);
CREATE INDEX ix_orders_created_status ON orders(created_at DESC, status);
順序が重要です! (user_id, status) のインデックスは、次のクエリに有効です。
WHERE user_id = ? AND status = ?✅WHERE user_id = ?✅WHERE status = ?❌(インデックスを使用しません)
部分インデックス
-- ✅ Index only relevant rows
CREATE INDEX ix_orders_pending ON orders(created_at)
WHERE status = 'Pending';
CREATE INDEX ix_users_active_email ON users(email)
WHERE is_deleted = false;
// EF Core
builder.HasIndex(o => o.CreatedAt)
.HasFilter("status = 'Pending'");
カバリングインデックス(INCLUDE)
-- ✅ Include frequently accessed columns
CREATE INDEX ix_users_email_include ON users(email)
INCLUDE (first_name, last_name);
全文検索インデックス
-- ✅ GIN index for full-text search
ALTER TABLE products
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
to_tsvector('english', coalesce(name, '') || ' ' || coalesce(description, ''))
) STORED;
CREATE INDEX ix_products_search ON products USING GIN(search_vector);
JSONB インデックス
-- ✅ GIN index for JSONB queries
CREATE INDEX ix_products_metadata ON products USING GIN(metadata);
-- Specific path index
CREATE INDEX ix_products_metadata_tags ON products USING GIN((metadata -> 'tags'));
インデックスを作成しない場合
❌ 次の場合はインデックスを作成しないでください。
- 非常に小さいテーブル(1000 行未満)
- WHERE/JOIN でほとんど使用されない列
- カーディナリティが低い列(異なる値が少ない列)
- 頻繁に変更される列
制約
主キー制約
-- ✅ Named primary key
CONSTRAINT pk_users PRIMARY KEY (id)
// EF Core names automatically: pk_{table}
builder.HasKey(u => u.Id);
外部キー制約
-- ✅ Named foreign keys with appropriate delete behavior
CONSTRAINT fk_orders_users
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE RESTRICT; -- Prevent orphans
CONSTRAINT fk_order_items_orders
FOREIGN KEY (order_id)
REFERENCES orders(id)
ON DELETE CASCADE; -- Delete items with order
builder.HasOne(o => o.User)
.WithMany(u => u.Orders)
.HasForeignKey(o => o.UserId)
.OnDelete(DeleteBehavior.Restrict);
チェック制約
-- ✅ Enforce business rules at database level
CONSTRAINT ck_users_age CHECK (age >= 18 AND age <= 120)
CONSTRAINT ck_products_price CHECK (price >= 0)
CONSTRAINT ck_orders_quantity CHECK (quantity > 0)
builder.ToTable(t => t.HasCheckConstraint(
"ck_products_price",
"price >= 0"));
一意制約
-- ✅ Composite unique constraints
CONSTRAINT uq_user_profiles_user_type UNIQUE (user_id, profile_type)
builder.HasIndex(p => new { p.UserId, p.ProfileType }).IsUnique();
パフォーマンス最適化
コネクションプーリング
// Connection string with pooling (default enabled)
"Host=localhost;Database=mydb;Username=postgres;Password=pass;Pooling=true;MinPoolSize=1;MaxPoolSize=100"
プリペアドステートメント
Npgsql は、繰り返し実行されるクエリに対してプリペアドステートメントを自動的に使用します。
// EF Core automatically prepares statements
var users = await context.Users
.Where(u => u.Email == email) // Prepared on first execution
.ToListAsync();
バッチ操作
// ✅ Batch inserts
context.Users.AddRange(users);
await context.SaveChangesAsync(); // Single round-trip
// ❌ Individual inserts
foreach (var user in users)
{
context.Users.Add(user);
await context.SaveChangesAsync(); // Multiple round-trips!
}
読み取り専用クエリには AsNoTracking を使用
// ✅ Read-only queries
var users = await context.Users
.AsNoTracking() // Faster, no change tracking
.ToListAsync();
// ✅ Write operations (default)
var user = await context.Users.FindAsync(id);
user.Update(...);
await context.SaveChangesAsync();
コンパイル済みクエリ
// Define once
private static readonly Func<ApplicationDbContext, string, Task<User?>> GetUserByEmail =
EF.CompileAsyncQuery((ApplicationDbContext context, string email) =>
context.Users.FirstOrDefault(u => u.Email == email));
// Use many times (faster)
var user = await GetUserByEmail(context, email);
ページネーション
-- ✅ Efficient pagination
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;
// ✅ EF Core pagination
var users = await context.Users
.OrderByDescending(u => u.CreatedAt)
.Skip(pageNumber * pageSize)
.Take(pageSize)
.ToListAsync();
N+1 クエリを回避
// ❌ N+1 problem
var orders = await context.Orders.ToListAsync();
foreach (var order in orders)
{
var user = await context.Users.FindAsync(order.UserId); // N queries!
}
// ✅ Eager loading
var orders = await context.Orders
.Include(o => o.User) // Single query with JOIN
.ToListAsync();
// ✅ Split query for multiple collections
var orders = await context.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.AsSplitQuery() // Separate queries, avoids cartesian explosion
.ToListAsync();
メンテナンス
Analyze と Vacuum
-- Analyze tables (update statistics)
ANALYZE users;
ANALYZE VERBOSE; -- All tables
-- Vacuum (reclaim space)
VACUUM users;
VACUUM ANALYZE users; -- Both operations
自動バキュームの設定
PostgreSQL は自動的に自動バキュームを実行しますが、必要に応じて調整してください。
-- Check autovacuum settings
SHOW autovacuum;
SHOW autovacuum_naptime;
-- Per-table autovacuum tuning
ALTER TABLE high_traffic_table SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_scale_factor = 0.02
);
インデックスの使用状況を監視する
-- Find unused indexes
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
-- Find missing indexes (sequential scans on large tables)
SELECT
schemaname,
tablename,
seq_scan,
seq_tup_read,
idx_scan,
seq_tup_read / seq_scan AS avg_seq_tup
FROM pg_stat_user_tables
WHERE seq_scan > 0
ORDER BY seq_tup_read DESC
LIMIT 20;
テーブルの肥大化
-- Check table bloat
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename) - pg_relation_size(schemaname||'.'||tablename)) AS external_size
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
セキュリティのベストプラクティス
接続のセキュリティ
// ✅ Use SSL/TLS
"Host=prod-server;Database=mydb;Username=app_user;Password=pass;SSL Mode=Require"
// ✅ Use connection pooling limits
"Host=localhost;Database=mydb;Username=postgres;Password=pass;MaxPoolSize=50;Timeout=30"
最小権限
-- ✅ Create application-specific user
CREATE USER app_user WITH PASSWORD 'secure_password';
-- Grant only necessary permissions
GRANT CONNECT ON DATABASE mydb TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;
-- ❌ Don't use superuser for application
-- Don't grant ALL PRIVILEGES
パラメーター化クエリ
// ✅ EF Core uses parameters automatically (safe from SQL injection)
var users = await context.Users
.Where(u => u.Email == email) // Parameterized
.ToListAsync();
// ✅ Dapper with parameters
var users = await connection.QueryAsync<User>(
"SELECT * FROM users WHERE email = @Email",
new { Email = email });
// ❌ NEVER concatenate SQL
var sql = $"SELECT * FROM users WHERE email = '{email}'"; // SQL INJECTION!
PostgreSQL 拡張機能
推奨される拡張機能
-- UUID generation
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
-- Full-text search
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Trigram matching
-- Case-insensitive text
CREATE EXTENSION IF NOT EXISTS "citext";
-- Additional types
CREATE EXTENSION IF NOT EXISTS "hstore"; -- Key-value store
CREATE EXTENSION IF NOT EXISTS "ltree"; -- Hierarchical data
拡張機能の使用
-- UUID generation
CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid()
);
-- Case-insensitive email
CREATE TABLE users (
id uuid PRIMARY KEY,
email citext UNIQUE NOT NULL
);
-- Trigram similarity search
CREATE INDEX ix_products_name_trgm ON products USING GIN(name gin_trgm_ops);
SELECT * FROM products
WHERE name % 'search term' -- Similarity operator
ORDER BY similarity(name, 'search term') DESC;
高い同時実行性を実現するパターン
xmin システム列を使用した楽観的同時実行制御
PostgreSQL には xmin を含む非表示のシステム列があり、xmin には最後に更新したトランザクションの ID が格納されます。これは楽観的同時実行制御に最適です。
// src/{name}.domain/{Entity}/{Entity}.cs
public class Order
{
public Guid Id { get; private set; }
public string Status { get; private set; }
// Concurrency token - maps to PostgreSQL xmin system column
public uint RowVersion { get; private set; }
}
// src/{name}.infrastructure/Configurations/OrderConfiguration.cs
internal sealed class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("orders");
builder.HasKey(o => o.Id);
// ═══════════════════════════════════════════════════════════════
// OPTIMISTIC CONCURRENCY WITH xmin
// PostgreSQL xmin system column auto-updates on every row change
// ═══════════════════════════════════════════════════════════════
builder.Property(o => o.RowVersion)
.IsRowVersion(); // Npgsql maps this to xmin automatically
}
}
同時実行の競合への対処
// src/{name}.infrastructure/Repositories/OrderRepository.cs
public async Task UpdateAsync(Order order, CancellationToken cancellationToken)
{
try
{
context.Orders.Update(order);
await context.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateConcurrencyException ex)
{
// Another user modified this row since we loaded it
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync(cancellationToken);
if (databaseValues is null)
{
throw new EntityNotFoundException("Order was deleted by another user");
}
// Option 1: Client wins - overwrite database values
// entry.OriginalValues.SetValues(databaseValues);
// await context.SaveChangesAsync(cancellationToken);
// Option 2: Database wins - throw and let caller retry
throw new ConcurrencyException("Order was modified by another user. Please refresh and try again.");
}
}
指数バックオフによる再試行(Polly)
// src/{name}.infrastructure/DependencyInjection.cs
using Polly;
using Polly.Retry;
// Configure retry policy for transient database errors
services.AddResiliencePipeline("database", builder =>
{
builder.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder()
.Handle<DbUpdateConcurrencyException>()
.Handle<NpgsqlException>(ex => ex.IsTransient),
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true, // Adds randomness to prevent thundering herd
OnRetry = static args =>
{
Console.WriteLine($"Retry attempt {args.AttemptNumber} after {args.RetryDelay}");
return default;
}
});
});
// Usage in handler
public class UpdateOrderHandler : ICommandHandler<UpdateOrderCommand, Unit>
{
private readonly ResiliencePipeline _pipeline;
public async Task<Unit> HandleAsync(UpdateOrderCommand command, CancellationToken ct)
{
await _pipeline.ExecuteAsync(async token =>
{
var order = await _repository.GetByIdAsync(command.OrderId, token);
order.Update(command.Status);
await _repository.UpdateAsync(order, token);
}, ct);
return Unit.Value;
}
}
高パフォーマンスを実現する DbContext プーリング
// src/{name}.infrastructure/DependencyInjection.cs
services.AddDbContextPool<ApplicationDbContext>(options =>
{
options.UseNpgsql(connectionString)
.UseSnakeCaseNamingConvention();
}, poolSize: 128); // Default is 1024, tune based on load
// For even higher performance, use PooledDbContextFactory
services.AddPooledDbContextFactory<ApplicationDbContext>(options =>
{
options.UseNpgsql(connectionString)
.UseSnakeCaseNamingConvention();
});
// Usage with PooledDbContextFactory (avoids DI overhead)
public class HighPerformanceQueryHandler
{
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;
public async Task<List<Order>> GetOrdersAsync(CancellationToken ct)
{
await using var context = await _contextFactory.CreateDbContextAsync(ct);
return await context.Orders
.AsNoTracking()
.Where(o => o.Status == "Active")
.ToListAsync(ct);
}
}
接続プーリングの設定
// Optimized connection string for high concurrency
var connectionString = new NpgsqlConnectionStringBuilder
{
Host = "localhost",
Database = "mydb",
Username = "app_user",
Password = "secret",
// ═══════════════════════════════════════════════════════════════
// CONNECTION POOLING SETTINGS
// ═══════════════════════════════════════════════════════════════
Pooling = true,
MinPoolSize = 10, // Keep 10 connections warm
MaxPoolSize = 100, // Maximum concurrent connections
ConnectionIdleLifetime = 300, // Close idle connections after 5 min
ConnectionPruningInterval = 10, // Check for idle every 10 sec
// ═══════════════════════════════════════════════════════════════
// TIMEOUT SETTINGS
// ═══════════════════════════════════════════════════════════════
Timeout = 30, // Connection timeout in seconds
CommandTimeout = 60, // Query timeout in seconds
// ═══════════════════════════════════════════════════════════════
// PERFORMANCE SETTINGS
// ═══════════════════════════════════════════════════════════════
MaxAutoPrepare = 20, // Auto-prepare frequently used queries
AutoPrepareMinUsages = 5, // Prepare after 5 uses
WriteBufferSize = 16384, // 16KB write buffer
ReadBufferSize = 16384, // 16KB read buffer
// ═══════════════════════════════════════════════════════════════
// MULTIPLEXING (Npgsql 6.0+)
// ═══════════════════════════════════════════════════════════════
Multiplexing = true, // Share connections for multiple commands
// ═══════════════════════════════════════════════════════════════
// SSL FOR PRODUCTION
// ═══════════════════════════════════════════════════════════════
SslMode = SslMode.Require,
TrustServerCertificate = false
}.ConnectionString;
重要な操作での行レベルロック
-- ✅ SELECT FOR UPDATE - Lock rows during transaction
SELECT * FROM accounts WHERE id = $1 FOR UPDATE;
-- ✅ Skip locked rows (for queue processing)
SELECT * FROM jobs
WHERE status = 'pending'
ORDER BY created_at
LIMIT 10
FOR UPDATE SKIP LOCKED;
-- ✅ NOWAIT - Fail immediately if locked
SELECT * FROM accounts WHERE id = $1 FOR UPDATE NOWAIT;
// EF Core equivalent using raw SQL
public async Task<Account?> GetForUpdateAsync(Guid id, CancellationToken ct)
{
return await context.Accounts
.FromSqlInterpolated($"SELECT * FROM accounts WHERE id = {id} FOR UPDATE")
.FirstOrDefaultAsync(ct);
}
// Queue processing with SKIP LOCKED
public async Task<List<Job>> GetPendingJobsAsync(int batchSize, CancellationToken ct)
{
return await context.Jobs
.FromSqlInterpolated($@"
SELECT * FROM jobs
WHERE status = 'pending'
ORDER BY created_at
LIMIT {batchSize}
FOR UPDATE SKIP LOCKED")
.ToListAsync(ct);
}
分散調整のためのアドバイザリロック
-- Application-level locks (not row-level)
-- Lock is held until session ends or explicitly released
-- Acquire blocking lock
SELECT pg_advisory_lock(12345);
-- Try to acquire (returns true/false immediately)
SELECT pg_try_advisory_lock(12345);
-- Release lock
SELECT pg_advisory_unlock(12345);
// EF Core advisory lock helper
public class AdvisoryLockService
{
private readonly ApplicationDbContext _context;
public async Task<bool> TryAcquireLockAsync(long lockId, CancellationToken ct)
{
var result = await _context.Database
.SqlQuery<bool>($"SELECT pg_try_advisory_lock({lockId})")
.FirstAsync(ct);
return result;
}
public async Task ReleaseLockAsync(long lockId, CancellationToken ct)
{
await _context.Database
.ExecuteSqlAsync($"SELECT pg_advisory_unlock({lockId})", ct);
}
}
// Usage for distributed singleton operations
public async Task ProcessDailyReportAsync(CancellationToken ct)
{
const long DAILY_REPORT_LOCK = 1001;
if (!await _lockService.TryAcquireLockAsync(DAILY_REPORT_LOCK, ct))
{
_logger.LogInformation("Another instance is processing the daily report");
return;
}
try
{
// Only one instance executes this
await GenerateReportAsync(ct);
}
finally
{
await _lockService.ReleaseLockAsync(DAILY_REPORT_LOCK, ct);
}
}
スレッドセーフティチェックの無効化(高パフォーマンス)
// ⚠️ Only after thorough testing!
// Saves ~50ns per operation but removes safety checks
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseNpgsql(connectionString)
.UseSnakeCaseNamingConvention()
.EnableThreadSafetyChecks(false); // Disable in production
});
重要なルール
- snake_case を使用する - PostgreSQL の標準です。EFCore.NamingConventions を使用してください
- varchar より text を使用する - 長さの制限が必要な場合を除きます
- 常に timestamptz を使用する - 時刻は UTC で保存してください
- 外部キーにインデックスを付ける - EF Core が自動的に設定します
- 主キーには uuid を使用する - 分散システムに適しています
- json ではなく jsonb を使用する - バイナリ形式のほうが高速です
- クエリをパラメーター化する - SQL インジェクションを防止します
- インデックスの使用状況を監視する - 使用されていないインデックスを削除してください
- 接続プーリングを使用する - 既定で有効です
- ANALYZE を定期的に実行する - 統計情報を最新の状態に保ってください
- 楽観的同時実行制御には xmin を使用する - 行が変更されるたびに自動更新されます
- バックオフを伴う再試行を実装する - 一時的な障害に適切に対処します
関連するスキル
06-dotnet-ef-core-configuration- EF Core のエンティティ設定05-dotnet-repository-pattern- データアクセスパターン19-dotnet-dapper-query-builder- Dapper を使用した生 SQL01-dotnet-clean-architecture- 全体的なアーキテクチャ
コメント
Markdown 対応