Massive
Massiveは、シンプルで動的な.NETデータアクセスライブラリです。単一ファイル実装で依存関係ゼロという究極の軽量性を実現し、プロトタイピングや小規模プロジェクトに最適化されています。動的型付けとシンプルなAPIにより、素早い開発とREPL環境での使用に適しています。
GitHub概要
FransBouma/Massive
A small, happy, dynamic MicroORM for .NET that will love you forever.
スター1,792
ウォッチ108
フォーク323
作成日:2011年2月15日
言語:C#
ライセンス:Other
トピックス
なし
スター履歴
データ取得日時: 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);
}
}