LINQ to DB
LINQ to DBは、軽量でSQL優先の.NET ORMライブラリです。LINQクエリの直接的なSQL変換により高いパフォーマンスを実現し、多様なデータベースプロバイダーをサポートします。Entity Frameworkの代替として、より直接的なデータベース制御とパフォーマンスを求める開発者に選ばれています。
ライブラリ
LINQ to DB
概要
LINQ to DBは、軽量でSQL優先の.NET ORMライブラリです。LINQクエリの直接的なSQL変換により高いパフォーマンスを実現し、多様なデータベースプロバイダーをサポートします。Entity Frameworkの代替として、より直接的なデータベース制御とパフォーマンスを求める開発者に選ばれています。
詳細
LINQ to DB 2025年版は、LINQ愛用者に人気の選択肢として確立されています。SQL制御とLINQ構文の両方が必要な開発者に支持され、特にパフォーマンス要件が厳しい場面で採用されています。複雑なSQLクエリをLINQ構文で表現できる一方、必要に応じて生のSQLも直接実行できる柔軟性を持ちます。SQL Server、PostgreSQL、MySQL、SQLite、Oracle、DB2など30以上のデータベースをサポートしています。
主な特徴
- 高速なLINQプロバイダー: 最適化されたSQL生成
- 豊富なデータベースサポート: 30以上のデータベース対応
- SQL優先設計: 直接的なSQL制御が可能
- バルク操作: 高速な一括挿入・更新・削除
- ストアドプロシージャサポート: 完全な統合
- 軽量: 最小限の依存関係とメモリフットプリント
メリット・デメリット
メリット
- Entity Frameworkより高速なクエリ実行
- 複雑なSQLクエリの完全な制御
- 幅広いデータベースプロバイダー対応
- バルク操作による優れたパフォーマンス
- 既存データベーススキーマとの優れた互換性
- 学習曲線が比較的緩やか
デメリット
- Entity Frameworkより少ない高度なORM機能
- 変更追跡機能がない
- 小規模なコミュニティとエコシステム
- マイグレーション機能が限定的
- 初期設定がEntity Frameworkより複雑
参考ページ
書き方の例
基本セットアップ
// NuGetパッケージインストール
// Install-Package linq2db
// Install-Package linq2db.SqlServer // SQL Server用
// Install-Package linq2db.PostgreSQL // PostgreSQL用
// モデル定義
using LinqToDB.Mapping;
using System;
[Table("Users")]
public class User
{
[PrimaryKey, Identity]
public int Id { get; set; }
[Column(Length = 100), NotNull]
public string Name { get; set; }
[Column(Length = 255), NotNull]
public string Email { get; set; }
[Column]
public int? Age { get; set; }
[Column]
public DateTime CreatedAt { get; set; }
}
[Table("Posts")]
public class Post
{
[PrimaryKey, Identity]
public int Id { get; set; }
[Column(Length = 200), NotNull]
public string Title { get; set; }
[Column(DataType = DataType.Text)]
public string Content { get; set; }
[Column]
public int AuthorId { get; set; }
[Column]
public DateTime CreatedAt { get; set; }
// Navigation property
[Association(ThisKey = nameof(AuthorId), OtherKey = nameof(User.Id))]
public User Author { get; set; }
}
// データベースコンテキスト
using LinqToDB;
using LinqToDB.Data;
public class BlogDb : DataConnection
{
public BlogDb() : base("BlogConnection") { }
public ITable<User> Users => this.GetTable<User>();
public ITable<Post> Posts => this.GetTable<Post>();
}
基本的なCRUD操作
using LinqToDB;
using System;
using System.Linq;
using System.Threading.Tasks;
public class UserRepository
{
private readonly BlogDb _db;
public UserRepository(BlogDb db)
{
_db = db;
}
// CREATE - 新規作成
public async Task<int> CreateUserAsync(User user)
{
user.CreatedAt = DateTime.UtcNow;
return await _db.InsertWithInt32IdentityAsync(user);
}
// READ - 読み取り
public async Task<User> GetUserByIdAsync(int id)
{
return await _db.Users
.FirstOrDefaultAsync(u => u.Id == id);
}
public async Task<List<User>> GetAllUsersAsync()
{
return await _db.Users
.OrderBy(u => u.Name)
.ToListAsync();
}
// UPDATE - 更新
public async Task<int> UpdateUserAsync(User user)
{
return await _db.UpdateAsync(user);
}
// 部分更新
public async Task<int> UpdateUserAgeAsync(int userId, int newAge)
{
return await _db.Users
.Where(u => u.Id == userId)
.Set(u => u.Age, newAge)
.UpdateAsync();
}
// DELETE - 削除
public async Task<int> DeleteUserAsync(int id)
{
return await _db.Users
.Where(u => u.Id == id)
.DeleteAsync();
}
// 複雑なクエリ
public async Task<List<User>> SearchUsersAsync(
string keyword,
int? minAge,
int? maxAge)
{
var query = _db.Users.AsQueryable();
if (!string.IsNullOrEmpty(keyword))
{
query = query.Where(u =>
u.Name.Contains(keyword) ||
u.Email.Contains(keyword));
}
if (minAge.HasValue)
{
query = query.Where(u => u.Age >= minAge.Value);
}
if (maxAge.HasValue)
{
query = query.Where(u => u.Age <= maxAge.Value);
}
return await query
.OrderBy(u => u.Name)
.ToListAsync();
}
}
高度な機能
using LinqToDB;
using LinqToDB.Linq;
using System.Linq;
public class AdvancedQueries
{
private readonly BlogDb _db;
public AdvancedQueries(BlogDb db)
{
_db = db;
}
// JOIN操作
public async Task<List<PostWithAuthor>> GetPostsWithAuthorsAsync()
{
var result = await (
from p in _db.Posts
join u in _db.Users on p.AuthorId equals u.Id
select new PostWithAuthor
{
PostId = p.Id,
Title = p.Title,
Content = p.Content,
AuthorName = u.Name,
AuthorEmail = u.Email,
CreatedAt = p.CreatedAt
}
).ToListAsync();
return result;
}
// 集計関数
public async Task<UserStatistics> GetUserStatisticsAsync()
{
var stats = await _db.Users
.Select(g => new UserStatistics
{
TotalUsers = _db.Users.Count(),
AverageAge = _db.Users.Average(u => u.Age) ?? 0,
MaxAge = _db.Users.Max(u => u.Age) ?? 0,
MinAge = _db.Users.Min(u => u.Age) ?? 0
})
.FirstOrDefaultAsync();
return stats;
}
// グループ化
public async Task<List<AgeGroupCount>> GetUsersByAgeGroupAsync()
{
var result = await (
from u in _db.Users
where u.Age.HasValue
group u by u.Age.Value / 10 into g
orderby g.Key
select new AgeGroupCount
{
AgeGroup = g.Key * 10,
Count = g.Count()
}
).ToListAsync();
return result;
}
// サブクエリ
public async Task<List<User>> GetActiveUsersAsync()
{
var activeUserIds = _db.Posts
.Where(p => p.CreatedAt >= DateTime.UtcNow.AddDays(-30))
.GroupBy(p => p.AuthorId)
.Where(g => g.Count() >= 5)
.Select(g => g.Key);
return await _db.Users
.Where(u => activeUserIds.Contains(u.Id))
.ToListAsync();
}
// バルク操作
public async Task BulkInsertUsersAsync(List<User> users)
{
// 高速バルクコピー
await _db.BulkCopyAsync(users);
}
public async Task BulkUpdateUsersAsync(List<User> users)
{
// バルク更新
foreach (var batch in users.Batch(1000))
{
await _db.UpdateAsync(batch);
}
}
// ストアドプロシージャ実行
public async Task<List<User>> ExecuteStoredProcAsync(string keyword)
{
return await _db.QueryProc<User>("SearchUsers",
new DataParameter("@Keyword", keyword))
.ToListAsync();
}
// 生のSQL実行
public async Task<List<CustomResult>> ExecuteRawSqlAsync()
{
var sql = @"
SELECT
u.Name as UserName,
COUNT(p.Id) as PostCount,
MAX(p.CreatedAt) as LastPostDate
FROM Users u
LEFT JOIN Posts p ON u.Id = p.AuthorId
GROUP BY u.Id, u.Name
HAVING COUNT(p.Id) > 0
ORDER BY PostCount DESC";
return await _db.Query<CustomResult>(sql).ToListAsync();
}
// トランザクション処理
public async Task<bool> TransferPostsAsync(
int fromUserId,
int toUserId)
{
using (var transaction = await _db.BeginTransactionAsync())
{
try
{
// 転送元ユーザー存在確認
var fromUser = await _db.Users
.FirstOrDefaultAsync(u => u.Id == fromUserId);
if (fromUser == null)
throw new InvalidOperationException("Source user not found");
// 転送先ユーザー存在確認
var toUser = await _db.Users
.FirstOrDefaultAsync(u => u.Id == toUserId);
if (toUser == null)
throw new InvalidOperationException("Target user not found");
// 投稿を転送
var updatedCount = await _db.Posts
.Where(p => p.AuthorId == fromUserId)
.Set(p => p.AuthorId, toUserId)
.UpdateAsync();
await transaction.CommitAsync();
return true;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
}
}
// DTOクラス
public class PostWithAuthor
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public string AuthorName { get; set; }
public string AuthorEmail { get; set; }
public DateTime CreatedAt { get; set; }
}
public class UserStatistics
{
public int TotalUsers { get; set; }
public double AverageAge { get; set; }
public int MaxAge { get; set; }
public int MinAge { get; set; }
}
public class AgeGroupCount
{
public int AgeGroup { get; set; }
public int Count { get; set; }
}
public class CustomResult
{
public string UserName { get; set; }
public int PostCount { get; set; }
public DateTime? LastPostDate { get; set; }
}
実用例
// ASP.NET Core Web APIでの使用例
using Microsoft.AspNetCore.Mvc;
using LinqToDB.Configuration;
// Startup.cs / Program.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// LINQ to DB設定
services.AddSingleton<IDataProvider>(SqlServerTools.GetDataProvider());
services.AddScoped<BlogDb>();
services.AddScoped<UserRepository>();
services.AddControllers();
}
}
// appsettings.json
{
"ConnectionStrings": {
"BlogConnection": "Server=localhost;Database=BlogDB;User Id=sa;Password=yourPassword;"
}
}
// Controllers/UsersController.cs
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly UserRepository _userRepository;
private readonly BlogDb _db;
public UsersController(UserRepository userRepository, BlogDb db)
{
_userRepository = userRepository;
_db = db;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<User>>> GetUsers(
[FromQuery] string keyword,
[FromQuery] int? minAge,
[FromQuery] int? maxAge,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10)
{
var query = _db.Users.AsQueryable();
// フィルタリング
if (!string.IsNullOrEmpty(keyword))
{
query = query.Where(u =>
u.Name.Contains(keyword) ||
u.Email.Contains(keyword));
}
if (minAge.HasValue)
query = query.Where(u => u.Age >= minAge.Value);
if (maxAge.HasValue)
query = query.Where(u => u.Age <= maxAge.Value);
// 総数取得
var totalCount = await query.CountAsync();
// ページネーション
var users = await query
.OrderBy(u => u.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
// ヘッダーに総数を含める
Response.Headers.Add("X-Total-Count", totalCount.ToString());
return Ok(users);
}
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUser(int id)
{
var user = await _userRepository.GetUserByIdAsync(id);
if (user == null)
return NotFound();
return Ok(user);
}
[HttpPost]
public async Task<ActionResult<User>> CreateUser(CreateUserDto dto)
{
var user = new User
{
Name = dto.Name,
Email = dto.Email,
Age = dto.Age
};
user.Id = await _userRepository.CreateUserAsync(user);
return CreatedAtAction(
nameof(GetUser),
new { id = user.Id },
user);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateUser(int id, UpdateUserDto dto)
{
var user = await _userRepository.GetUserByIdAsync(id);
if (user == null)
return NotFound();
user.Name = dto.Name;
user.Email = dto.Email;
user.Age = dto.Age;
await _userRepository.UpdateUserAsync(user);
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteUser(int id)
{
var deleted = await _userRepository.DeleteUserAsync(id);
if (deleted == 0)
return NotFound();
return NoContent();
}
[HttpPost("bulk")]
public async Task<IActionResult> BulkCreateUsers(List<CreateUserDto> dtos)
{
var users = dtos.Select(dto => new User
{
Name = dto.Name,
Email = dto.Email,
Age = dto.Age,
CreatedAt = DateTime.UtcNow
}).ToList();
await _db.BulkCopyAsync(users);
return Ok(new { Count = users.Count });
}
[HttpGet("statistics")]
public async Task<ActionResult> GetStatistics()
{
var stats = await _db.Users
.Select(u => new
{
TotalUsers = _db.Users.Count(),
UsersWithPosts = _db.Posts.Select(p => p.AuthorId).Distinct().Count(),
AverageAge = _db.Users.Where(x => x.Age.HasValue).Average(x => x.Age),
TotalPosts = _db.Posts.Count()
})
.FirstOrDefaultAsync();
return Ok(stats);
}
}
// 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; }
}
// リポジトリパターンとUnitOfWork
public interface IUnitOfWork : IDisposable
{
BlogDb Database { get; }
Task<int> CommitAsync();
Task RollbackAsync();
}
public class UnitOfWork : IUnitOfWork
{
private BlogDb _database;
private IDbTransaction _transaction;
public BlogDb Database => _database;
public UnitOfWork(string connectionString)
{
_database = new BlogDb();
_transaction = _database.BeginTransaction();
}
public async Task<int> CommitAsync()
{
try
{
await _transaction.CommitAsync();
return 0;
}
catch
{
await _transaction.RollbackAsync();
throw;
}
}
public async Task RollbackAsync()
{
await _transaction.RollbackAsync();
}
public void Dispose()
{
_transaction?.Dispose();
_database?.Dispose();
}
}