PetaPoco

PetaPocoは「軽量で高速なmicro ORM」として開発された、.NET向けの単一ファイルで構成されるシンプルなデータアクセスライブラリです。Entity FrameworkやNHibernateのような重厚なORMとは対照的に、シンプルさとパフォーマンスを重視した設計が特徴。SQLの知識を活かしながら手動コーディングを大幅に削減し、SQLのフルパワーを制限することなく、安全で効率的なデータベース操作を提供します。

micro ORMC#.NETSQLiteMySQLPostgreSQLSQL Server

GitHub概要

CollaboratingPlatypus/PetaPoco

Official PetaPoco, A tiny ORM-ish thing for your POCO's

スター2,112
ウォッチ155
フォーク602
作成日:2011年3月18日
言語:C#
ライセンス:Other

トピックス

dotnetorm

スター履歴

CollaboratingPlatypus/PetaPoco Star History
データ取得日時: 2025/7/19 02:41

ライブラリ

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);
}