PetaPoco
PetaPocoは「軽量で高速なmicro ORM」として開発された、.NET向けの単一ファイルで構成されるシンプルなデータアクセスライブラリです。Entity FrameworkやNHibernateのような重厚なORMとは対照的に、シンプルさとパフォーマンスを重視した設計が特徴。SQLの知識を活かしながら手動コーディングを大幅に削減し、SQLのフルパワーを制限することなく、安全で効率的なデータベース操作を提供します。
GitHub概要
CollaboratingPlatypus/PetaPoco
Official PetaPoco, A tiny ORM-ish thing for your POCO's
トピックス
スター履歴
ライブラリ
PetaPoco
概要
PetaPocoは「軽量で高速なmicro ORM」として開発された、.NET向けの単一ファイルで構成されるシンプルなデータアクセスライブラリです。Entity FrameworkやNHibernateのような重厚なORMとは対照的に、シンプルさとパフォーマンスを重視した設計が特徴。SQLの知識を活かしながら手動コーディングを大幅に削減し、SQLのフルパワーを制限することなく、安全で効率的なデータベース操作を提供します。
詳細
PetaPoco 2025年版は.NET開発エコシステムにおいて確固たる地位を築いた軽量ORMソリューションです。単一C#ファイルとして配布され、依存関係がまったくないため導入が極めて簡単。動的メソッド生成(MSIL)によりDapperと同等の高速性能を実現し、厳密に型付けされたPOCOオブジェクトまたは動的オブジェクトの両方をサポート。SQL Server、PostgreSQL、MySQL、SQLiteなど主要データベースを幅広くサポートし、.NET Standard 2.0、.NET 4.0/4.5+、Monoで動作します。
主な特徴
- 単一ファイル実装: 依存関係なしの単一C#ファイルで構成
- 高速パフォーマンス: MSILによる動的メソッド生成で最適化
- POCO対応: 属性なしまたは最小限の属性でのPOCOオブジェクト操作
- SQLフルサポート: 複雑なカスタムクエリの完全サポート
- 多データベース対応: 主要RDBMS全般をサポート
- T4テンプレート: クラス生成の自動化サポート
メリット・デメリット
メリット
- 極めて軽量で依存関係がなく、プロジェクトへの組み込みが簡単
- DapperやEF Coreに匹敵する高いパフォーマンス
- SQLの知識を活かせるため学習コストが低い
- 複雑なクエリでもSQLを直接記述可能で制限なし
- .NET Standard対応により幅広いプラットフォームで使用可能
- ページング機能など便利なヘルパーメソッド内蔵
デメリット
- 本格的なORMと比較して機能が限定的
- リレーションシップやマイグレーション管理機能なし
- 手動でのSQL記述とオブジェクトマッピングが必要
- 大規模なエンタープライズ機能(キャッシュ、遅延読み込み等)は非対応
- Entity FrameworkのようなLINQ統合やモデル駆動開発は不可
- 開発効率はフル機能ORMより劣る場合がある
参考ページ
書き方の例
セットアップ
// NuGet パッケージインストール
// Install-Package PetaPoco
// または、単一ファイルとしてプロジェクトに直接追加
// PetaPoco.cs をプロジェクトにコピー
using PetaPoco;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
基本的な使い方
// POCOクラスの定義
[TableName("Users")]
[PrimaryKey("Id", AutoIncrement = true)]
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsActive { get; set; }
}
[TableName("Posts")]
[PrimaryKey("Id", AutoIncrement = true)]
public class Post
{
public int Id { get; set; }
public int UserId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime CreatedAt { get; set; }
}
// データベース接続とCRUD操作
public class UserRepository
{
private readonly string _connectionString;
public UserRepository(string connectionString)
{
_connectionString = connectionString;
}
public async Task<List<User>> GetAllUsersAsync()
{
using var db = new Database(_connectionString, "System.Data.SqlClient");
return await db.FetchAsync<User>("SELECT * FROM Users ORDER BY CreatedAt DESC");
}
public async Task<User> GetUserByIdAsync(int id)
{
using var db = new Database(_connectionString, "System.Data.SqlClient");
return await db.SingleOrDefaultAsync<User>("SELECT * FROM Users WHERE Id = @0", id);
}
public async Task<int> CreateUserAsync(User user)
{
using var db = new Database(_connectionString, "System.Data.SqlClient");
user.CreatedAt = DateTime.Now;
return (int)await db.InsertAsync(user);
}
public async Task<bool> UpdateUserAsync(User user)
{
using var db = new Database(_connectionString, "System.Data.SqlClient");
return await db.UpdateAsync(user) > 0;
}
public async Task<bool> DeleteUserAsync(int id)
{
using var db = new Database(_connectionString, "System.Data.SqlClient");
return await db.DeleteAsync<User>(id) > 0;
}
}
クエリ実行
public class AdvancedUserService
{
private readonly string _connectionString;
public AdvancedUserService(string connectionString)
{
_connectionString = connectionString;
}
// 条件付き検索
public async Task<List<User>> SearchUsersAsync(string nameFilter, int? minAge = null, int? maxAge = null)
{
using var db = new Database(_connectionString, "System.Data.SqlClient");
var sql = new Sql("SELECT * FROM Users WHERE 1=1");
if (!string.IsNullOrEmpty(nameFilter))
sql = sql.Append("AND Name LIKE @0", $"%{nameFilter}%");
if (minAge.HasValue)
sql = sql.Append("AND Age >= @0", minAge.Value);
if (maxAge.HasValue)
sql = sql.Append("AND Age <= @0", maxAge.Value);
sql = sql.Append("ORDER BY Name");
return await db.FetchAsync<User>(sql);
}
// ページング
public async Task<Page<User>> GetUsersPagedAsync(int page, int itemsPerPage)
{
using var db = new Database(_connectionString, "System.Data.SqlClient");
return await db.PageAsync<User>(page, itemsPerPage, "SELECT * FROM Users ORDER BY CreatedAt DESC");
}
// 複雑なJOINクエリ
public async Task<List<UserWithPostCount>> GetUsersWithPostCountAsync()
{
using var db = new Database(_connectionString, "System.Data.SqlClient");
var sql = @"
SELECT u.*, COUNT(p.Id) as PostCount
FROM Users u
LEFT JOIN Posts p ON u.Id = p.UserId
WHERE u.IsActive = 1
GROUP BY u.Id, u.Name, u.Email, u.Age, u.CreatedAt, u.IsActive
ORDER BY PostCount DESC";
return await db.FetchAsync<UserWithPostCount>(sql);
}
// カスタムマッピング
public async Task<List<UserSummary>> GetUserSummariesAsync()
{
using var db = new Database(_connectionString, "System.Data.SqlClient");
return await db.FetchAsync<UserSummary>(@"
SELECT
Id,
Name,
Email,
CASE WHEN Age >= 18 THEN 'Adult' ELSE 'Minor' END as AgeGroup,
DATEDIFF(YEAR, CreatedAt, GETDATE()) as YearsSinceJoined
FROM Users
ORDER BY CreatedAt DESC");
}
}
// カスタムPOCOクラス
public class UserWithPostCount
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsActive { get; set; }
public int PostCount { get; set; }
}
public class UserSummary
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string AgeGroup { get; set; }
public int YearsSinceJoined { get; set; }
}
データ操作
// 実用的な使用例
public class BlogService
{
private readonly string _connectionString;
public BlogService(string connectionString)
{
_connectionString = connectionString;
}
// 投稿作成(ユーザーチェック付き)
public async Task<int> CreatePostAsync(int userId, string title, string content)
{
using var db = new Database(_connectionString, "System.Data.SqlClient");
// ユーザー存在確認
var user = await db.SingleOrDefaultAsync<User>("SELECT * FROM Users WHERE Id = @0 AND IsActive = 1", userId);
if (user == null)
throw new ArgumentException("Active user not found");
var post = new Post
{
UserId = userId,
Title = title,
Content = content,
CreatedAt = DateTime.Now
};
return (int)await db.InsertAsync(post);
}
// バルク操作
public async Task<int> CreateMultipleUsersAsync(List<User> users)
{
using var db = new Database(_connectionString, "System.Data.SqlClient");
int insertedCount = 0;
using (var transaction = db.GetTransaction())
{
try
{
foreach (var user in users)
{
user.CreatedAt = DateTime.Now;
await db.InsertAsync(user);
insertedCount++;
}
transaction.Complete();
}
catch
{
transaction.Dispose(); // ロールバック
throw;
}
}
return insertedCount;
}
// 統計情報取得
public async Task<BlogStatistics> GetBlogStatisticsAsync()
{
using var db = new Database(_connectionString, "System.Data.SqlClient");
var stats = await db.SingleAsync<BlogStatistics>(@"
SELECT
(SELECT COUNT(*) FROM Users WHERE IsActive = 1) as ActiveUserCount,
(SELECT COUNT(*) FROM Users WHERE IsActive = 0) as InactiveUserCount,
(SELECT COUNT(*) FROM Posts) as TotalPostCount,
(SELECT COUNT(*) FROM Posts WHERE CreatedAt >= DATEADD(day, -30, GETDATE())) as RecentPostCount,
(SELECT AVG(CAST(Age as FLOAT)) FROM Users WHERE IsActive = 1) as AverageUserAge");
return stats;
}
}
public class BlogStatistics
{
public int ActiveUserCount { get; set; }
public int InactiveUserCount { get; set; }
public int TotalPostCount { get; set; }
public int RecentPostCount { get; set; }
public double AverageUserAge { get; set; }
}
設定とカスタマイズ
// データベース設定の管理
public class DatabaseConfiguration
{
public static Database CreateDatabase(string connectionString, string providerName = "System.Data.SqlClient")
{
var db = new Database(connectionString, providerName);
// グローバル設定
db.CommandTimeout = 30; // 30秒のタイムアウト
// SQLログの有効化(開発環境のみ)
#if DEBUG
db.OnExecutingCommand = command =>
{
Console.WriteLine($"Executing: {command.CommandText}");
foreach (var param in command.Parameters.Cast<IDataParameter>())
{
Console.WriteLine($" {param.ParameterName}: {param.Value}");
}
};
#endif
return db;
}
// マルチデータベース対応
public static Database CreateSQLiteDatabase(string filePath)
{
var connectionString = $"Data Source={filePath};Version=3;";
return new Database(connectionString, "System.Data.SQLite");
}
public static Database CreateMySQLDatabase(string server, string database, string username, string password)
{
var connectionString = $"Server={server};Database={database};Uid={username};Pwd={password};";
return new Database(connectionString, "MySql.Data.MySqlClient");
}
public static Database CreatePostgreSQLDatabase(string host, string database, string username, string password)
{
var connectionString = $"Host={host};Database={database};Username={username};Password={password};";
return new Database(connectionString, "Npgsql");
}
}
// カスタムPOCO Mapper
public class CustomMappingExample
{
public class UserProfile
{
public int UserId { get; set; }
public string FullName { get; set; }
public string ContactInfo { get; set; }
public DateTime LastActivity { get; set; }
}
public async Task<List<UserProfile>> GetUserProfilesAsync(string connectionString)
{
using var db = DatabaseConfiguration.CreateDatabase(connectionString);
// カスタムマッピングロジック
return await db.FetchAsync<UserProfile>(@"
SELECT
u.Id as UserId,
CONCAT(u.Name, ' (', u.Email, ')') as FullName,
u.Email as ContactInfo,
COALESCE(p.MaxCreatedAt, u.CreatedAt) as LastActivity
FROM Users u
LEFT JOIN (
SELECT UserId, MAX(CreatedAt) as MaxCreatedAt
FROM Posts
GROUP BY UserId
) p ON u.Id = p.UserId
WHERE u.IsActive = 1
ORDER BY LastActivity DESC");
}
}
エラーハンドリング
public class ErrorHandlingService
{
private readonly string _connectionString;
private readonly ILogger _logger;
public ErrorHandlingService(string connectionString, ILogger logger)
{
_connectionString = connectionString;
_logger = logger;
}
public async Task<Result<User>> CreateUserSafelyAsync(User user)
{
try
{
using var db = DatabaseConfiguration.CreateDatabase(_connectionString);
// 重複チェック
var existingUser = await db.SingleOrDefaultAsync<User>(
"SELECT * FROM Users WHERE Email = @0", user.Email);
if (existingUser != null)
{
return Result<User>.Failure("Email already exists");
}
user.CreatedAt = DateTime.Now;
user.Id = (int)await db.InsertAsync(user);
_logger.LogInformation($"User created successfully: {user.Id}");
return Result<User>.Success(user);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create user: {Email}", user.Email);
return Result<User>.Failure($"Database error: {ex.Message}");
}
}
public async Task<Result<int>> BulkInsertWithRetryAsync(List<User> users, int maxRetries = 3)
{
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
using var db = DatabaseConfiguration.CreateDatabase(_connectionString);
int insertedCount = 0;
using (var transaction = db.GetTransaction())
{
foreach (var user in users)
{
user.CreatedAt = DateTime.Now;
await db.InsertAsync(user);
insertedCount++;
}
transaction.Complete();
}
return Result<int>.Success(insertedCount);
}
catch (Exception ex) when (attempt < maxRetries)
{
_logger.LogWarning(ex, "Bulk insert attempt {Attempt} failed, retrying...", attempt);
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); // Exponential backoff
}
catch (Exception ex)
{
_logger.LogError(ex, "Bulk insert failed after {MaxRetries} attempts", maxRetries);
return Result<int>.Failure($"Failed after {maxRetries} attempts: {ex.Message}");
}
}
return Result<int>.Failure("Unexpected error");
}
}
// 結果クラス
public class Result<T>
{
public bool IsSuccess { get; private set; }
public T Data { get; private set; }
public string Error { get; private set; }
private Result(bool isSuccess, T data, string error)
{
IsSuccess = isSuccess;
Data = data;
Error = error;
}
public static Result<T> Success(T data) => new(true, data, null);
public static Result<T> Failure(string error) => new(false, default, error);
}