Massive

Massiveは、シンプルで動的な.NETデータアクセスライブラリです。単一ファイル実装で依存関係ゼロという究極の軽量性を実現し、プロトタイピングや小規模プロジェクトに最適化されています。動的型付けとシンプルなAPIにより、素早い開発とREPL環境での使用に適しています。

ORMC#動的マイクロORMシンプル軽量.NET

GitHub概要

FransBouma/Massive

A small, happy, dynamic MicroORM for .NET that will love you forever.

スター1,792
ウォッチ108
フォーク323
作成日:2011年2月15日
言語:C#
ライセンス:Other

トピックス

なし

スター履歴

FransBouma/Massive Star History
データ取得日時: 2025/7/19 09:28

ライブラリ

Massive

概要

Massiveは、シンプルで動的な.NETデータアクセスライブラリです。単一ファイル実装で依存関係ゼロという究極の軽量性を実現し、プロトタイピングや小規模プロジェクトに最適化されています。動的型付けとシンプルなAPIにより、素早い開発とREPL環境での使用に適しています。

詳細

Massive 2025年版は、シンプルさを重視するプロジェクトで継続利用されています。プロトタイピングやREPL環境での開発において重宝されており、Rob ConeryによるオリジナルのMassiveの精神を受け継ぎながら、Frans Boumaによってメンテナンスされています。SQL Server、PostgreSQL、MySQL、SQLiteをサポートし、動的なExpandoObjectベースのアプローチにより、スキーマの変更に柔軟に対応できます。

主な特徴

  • 単一ファイル実装: 1つのC#ファイルだけで完結
  • 依存関係ゼロ: 外部ライブラリ不要
  • 動的型付け: ExpandoObjectによる柔軟なデータアクセス
  • シンプルなAPI: 直感的で学習コストが最小
  • マルチデータベース: SQL Server、PostgreSQL、MySQL、SQLite対応
  • 軽量: 最小限のメモリフットプリント

メリット・デメリット

メリット

  • 導入が極めて簡単(ファイルをコピーするだけ)
  • 学習曲線がほぼ平坦
  • プロトタイピングに最適
  • スキーマ変更に柔軟に対応
  • REPL環境での使用に適している
  • 小規模プロジェクトでのオーバーヘッドが最小

デメリット

  • 型安全性の欠如
  • インテリセンスサポートが限定的
  • 大規模プロジェクトには不向き
  • パフォーマンス最適化オプションが少ない
  • 高度なORM機能(リレーション管理等)なし
  • エンタープライズ機能の不足

参考ページ

書き方の例

基本セットアップ

// Massive.csファイルをプロジェクトに追加するだけ
// NuGetパッケージは不要

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using Massive;

// テーブルクラスの定義
public class Users : DynamicModel
{
    public Users() : base("BlogConnection", "Users", "Id") { }
}

public class Posts : DynamicModel
{
    public Posts() : base("BlogConnection", "Posts", "Id") { }
}

// app.config / appsettings.json
// <connectionStrings>
//   <add name="BlogConnection" 
//        connectionString="Server=localhost;Database=BlogDB;..." />
// </connectionStrings>

基本的なCRUD操作

public class BasicOperations
{
    public static void DemoCRUD()
    {
        var users = new Users();
        
        // CREATE - 新規作成
        // 動的オブジェクトで作成
        var newUser = new ExpandoObject() as dynamic;
        newUser.Name = "田中太郎";
        newUser.Email = "[email protected]";
        newUser.Age = 30;
        newUser.CreatedAt = DateTime.Now;
        
        var userId = users.Insert(newUser);
        Console.WriteLine($"Created user with ID: {userId}");
        
        // 匿名オブジェクトでも作成可能
        var anotherUser = users.Insert(new {
            Name = "佐藤花子",
            Email = "[email protected]",
            Age = 25,
            CreatedAt = DateTime.Now
        });
        
        // READ - 読み取り
        // 単一レコード取得
        dynamic user = users.Single(userId);
        Console.WriteLine($"User: {user.Name}, Email: {user.Email}");
        
        // 全件取得
        var allUsers = users.All();
        foreach (dynamic u in allUsers)
        {
            Console.WriteLine($"{u.Name} ({u.Age})");
        }
        
        // 条件検索
        var adults = users.All(where: "Age >= @0", args: 18);
        var searchResults = users.All(
            where: "Name LIKE @0 OR Email LIKE @0", 
            args: "%田中%"
        );
        
        // UPDATE - 更新
        user.Age = 31;
        user.UpdatedAt = DateTime.Now;
        users.Update(user, userId);
        
        // 部分更新
        users.Update(new { Age = 32 }, userId);
        
        // DELETE - 削除
        users.Delete(userId);
        
        // 条件付き削除
        users.Delete(where: "CreatedAt < @0", args: DateTime.Now.AddYears(-1));
    }
}

高度なクエリ操作

public class AdvancedQueries
{
    private readonly Users _users = new Users();
    private readonly Posts _posts = new Posts();
    
    // ページネーション
    public IEnumerable<dynamic> GetPagedUsers(int page, int pageSize)
    {
        var offset = (page - 1) * pageSize;
        
        // SQL Server
        var sql = @"
            SELECT * FROM Users 
            ORDER BY Name 
            OFFSET @0 ROWS FETCH NEXT @1 ROWS ONLY";
        
        return _users.Query(sql, offset, pageSize);
    }
    
    // 集計クエリ
    public dynamic GetUserStatistics()
    {
        var sql = @"
            SELECT 
                COUNT(*) as TotalUsers,
                AVG(CAST(Age as float)) as AverageAge,
                MAX(Age) as MaxAge,
                MIN(Age) as MinAge
            FROM Users
            WHERE Age IS NOT NULL";
        
        return _users.Query(sql).FirstOrDefault();
    }
    
    // JOIN操作
    public IEnumerable<dynamic> GetPostsWithAuthors()
    {
        var sql = @"
            SELECT 
                p.Id,
                p.Title,
                p.Content,
                p.CreatedAt,
                u.Name as AuthorName,
                u.Email as AuthorEmail
            FROM Posts p
            INNER JOIN Users u ON p.AuthorId = u.Id
            ORDER BY p.CreatedAt DESC";
        
        return _posts.Query(sql);
    }
    
    // グループ化
    public IEnumerable<dynamic> GetPostCountByUser()
    {
        var sql = @"
            SELECT 
                u.Id,
                u.Name,
                COUNT(p.Id) as PostCount
            FROM Users u
            LEFT JOIN Posts p ON u.Id = p.AuthorId
            GROUP BY u.Id, u.Name
            ORDER BY PostCount DESC";
        
        return _users.Query(sql);
    }
    
    // 動的条件構築
    public IEnumerable<dynamic> SearchUsers(
        string name = null, 
        int? minAge = null, 
        int? maxAge = null)
    {
        var conditions = new List<string>();
        var parameters = new List<object>();
        
        if (!string.IsNullOrEmpty(name))
        {
            conditions.Add("Name LIKE @" + parameters.Count);
            parameters.Add($"%{name}%");
        }
        
        if (minAge.HasValue)
        {
            conditions.Add("Age >= @" + parameters.Count);
            parameters.Add(minAge.Value);
        }
        
        if (maxAge.HasValue)
        {
            conditions.Add("Age <= @" + parameters.Count);
            parameters.Add(maxAge.Value);
        }
        
        var where = conditions.Any() 
            ? "WHERE " + string.Join(" AND ", conditions) 
            : "";
        
        var sql = $"SELECT * FROM Users {where} ORDER BY Name";
        
        return _users.Query(sql, parameters.ToArray());
    }
    
    // バッチ操作
    public void BatchInsertUsers(List<dynamic> users)
    {
        // トランザクション内でバッチ挿入
        using (var conn = _users.OpenConnection())
        using (var trans = conn.BeginTransaction())
        {
            try
            {
                foreach (var user in users)
                {
                    _users.Insert(user, conn, trans);
                }
                trans.Commit();
            }
            catch
            {
                trans.Rollback();
                throw;
            }
        }
    }
    
    // カスタムSQL実行
    public int ExecuteCustomCommand(string sql, params object[] args)
    {
        return _users.Execute(sql, args);
    }
}

実用的な使用例

// RESTful APIでの使用例
using Microsoft.AspNetCore.Mvc;
using System.Dynamic;

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly Users _users = new Users();
    
    [HttpGet]
    public IActionResult GetUsers(
        [FromQuery] string search,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 10)
    {
        try
        {
            var offset = (page - 1) * pageSize;
            var where = string.IsNullOrEmpty(search) 
                ? "" 
                : "WHERE Name LIKE @2 OR Email LIKE @2";
            
            var sql = $@"
                SELECT * FROM Users {where}
                ORDER BY Name 
                OFFSET @0 ROWS FETCH NEXT @1 ROWS ONLY";
            
            var users = _users.Query(sql, offset, pageSize, $"%{search}%");
            
            // 総数取得
            var countSql = $"SELECT COUNT(*) as Count FROM Users {where}";
            var totalCount = string.IsNullOrEmpty(search)
                ? _users.Scalar(countSql)
                : _users.Scalar(countSql, $"%{search}%");
            
            Response.Headers.Add("X-Total-Count", totalCount.ToString());
            
            return Ok(users);
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { error = ex.Message });
        }
    }
    
    [HttpGet("{id}")]
    public IActionResult GetUser(int id)
    {
        try
        {
            var user = _users.Single(id);
            if (user == null)
                return NotFound();
            
            return Ok(user);
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { error = ex.Message });
        }
    }
    
    [HttpPost]
    public IActionResult CreateUser([FromBody] CreateUserDto dto)
    {
        try
        {
            dynamic user = new ExpandoObject();
            user.Name = dto.Name;
            user.Email = dto.Email;
            user.Age = dto.Age;
            user.CreatedAt = DateTime.UtcNow;
            
            var id = _users.Insert(user);
            user.Id = id;
            
            return CreatedAtAction(nameof(GetUser), new { id }, user);
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { error = ex.Message });
        }
    }
    
    [HttpPut("{id}")]
    public IActionResult UpdateUser(int id, [FromBody] UpdateUserDto dto)
    {
        try
        {
            var existing = _users.Single(id);
            if (existing == null)
                return NotFound();
            
            _users.Update(new
            {
                Name = dto.Name,
                Email = dto.Email,
                Age = dto.Age,
                UpdatedAt = DateTime.UtcNow
            }, id);
            
            return NoContent();
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { error = ex.Message });
        }
    }
    
    [HttpDelete("{id}")]
    public IActionResult DeleteUser(int id)
    {
        try
        {
            var result = _users.Delete(id);
            if (result == 0)
                return NotFound();
            
            return NoContent();
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { error = ex.Message });
        }
    }
}

// DTOクラス
public class CreateUserDto
{
    public string Name { get; set; }
    public string Email { get; set; }
    public int? Age { get; set; }
}

public class UpdateUserDto
{
    public string Name { get; set; }
    public string Email { get; set; }
    public int? Age { get; set; }
}

// ヘルパークラス
public static class MassiveExtensions
{
    // 動的オブジェクトを型付きオブジェクトに変換
    public static T ToTyped<T>(this dynamic obj) where T : new()
    {
        var typed = new T();
        var dict = obj as IDictionary<string, object>;
        
        if (dict != null)
        {
            var properties = typeof(T).GetProperties();
            foreach (var prop in properties)
            {
                if (dict.ContainsKey(prop.Name) && dict[prop.Name] != null)
                {
                    prop.SetValue(typed, dict[prop.Name]);
                }
            }
        }
        
        return typed;
    }
    
    // 動的オブジェクトのリストを型付きリストに変換
    public static List<T> ToTypedList<T>(this IEnumerable<dynamic> list) 
        where T : new()
    {
        return list.Select(item => ToTyped<T>(item)).ToList();
    }
}

// サービスクラスでの使用例
public class BlogService
{
    private readonly Users _users = new Users();
    private readonly Posts _posts = new Posts();
    
    public async Task<dynamic> CreateBlogPost(
        string title, 
        string content, 
        int authorId)
    {
        // ユーザー存在確認
        var author = _users.Single(authorId);
        if (author == null)
            throw new InvalidOperationException("Author not found");
        
        // 投稿作成
        var post = new ExpandoObject() as dynamic;
        post.Title = title;
        post.Content = content;
        post.AuthorId = authorId;
        post.CreatedAt = DateTime.UtcNow;
        post.ViewCount = 0;
        
        var postId = _posts.Insert(post);
        
        // 作成した投稿を返す
        return _posts.Query(@"
            SELECT 
                p.*,
                u.Name as AuthorName
            FROM Posts p
            INNER JOIN Users u ON p.AuthorId = u.Id
            WHERE p.Id = @0", postId).FirstOrDefault();
    }
    
    public IEnumerable<dynamic> GetPopularPosts(int limit = 10)
    {
        return _posts.Query(@"
            SELECT TOP(@0)
                p.Id,
                p.Title,
                p.ViewCount,
                p.CreatedAt,
                u.Name as AuthorName,
                (SELECT COUNT(*) FROM Comments WHERE PostId = p.Id) as CommentCount
            FROM Posts p
            INNER JOIN Users u ON p.AuthorId = u.Id
            ORDER BY p.ViewCount DESC, p.CreatedAt DESC", limit);
    }
    
    public void IncrementViewCount(int postId)
    {
        _posts.Execute(
            "UPDATE Posts SET ViewCount = ViewCount + 1 WHERE Id = @0", 
            postId);
    }
}