【C#】Entity Framework Core(EF Core)について
はじめに
本記事では、C# の ORM(オブジェクトリレーショナルマッピング)フレームワークである Entity Framework Core(EF Core)について解説します。
本記事の対象
- C#を用いてこれから開発をしていく方
- EF Coreについて知らない方
概要
EF Core は、.NET アプリケーションとリレーショナルデータベース間のデータアクセスを容易にし、可読性/保守性の高いコード記述を実現します。
具体的には以下のようなことが可能になります。
- LINQ を利用した直感的なSQLクエリ作成
- マイグレーションを用いた、データベース作成(コードファースト)
- スキャフォールドを用いた、データベースエンティティ作成(データベースファースト)
前提環境
- .net 8.0を想定
- 対象データベースはPostgreSQL(ver 16.2)
- 既にプロジェクト作成済みで、以下のNuGetパッケージがインストールされていること
ライブラリ名 | バージョン | 説明 |
---|---|---|
Microsoft.EntityFrameworkCore.Tools |
8.0.13 | Entity Framework Coreのコマンドラインツールを提供し、実際のデータベース操作を行うために李良い |
Microsoft.EntityFrameworkCore.Design |
8.0.13 | Entity Framework Coreにおけるマイグレーションを行うために利用 |
Npgsql.EntityFrameworkCore.PostgreSQL |
8.0.11 | Entity Framework Coreにおける、PostgreSQLデータベース用のプロバイダーとして利用 |
マイグレーション
マイグレーションは、データベーススキーマの変更をコードベースで管理するための仕組みです。EF Core では、以下の手順でマイグレーションを実現できます。
いわゆる、コードファーストの考え方です。
前提
- 接続先データベースにProductsテーブルが存在しないこと
手順
エンティティを定義
1. - Idは主キーかつ最大文字数は10文字とする。
- Nameは必須項目かつ最大文字数は100文字とする。
public class Product{
[Key] // 主キーの指定
[MaxLength(10)] // 最大10文字制限
public string Id { get; set; }
[Required] // 必須制約
[MaxLength(100)] // 最大100文字制限
public string Name { get; set; }
}
DbContextクラスを定義
2. using Microsoft.EntityFrameworkCore;
public class ApplicationDbContext : DbContext
{
// DbSetによりProductテーブルが定義される
public DbSet<Product> Products { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
接続先情報を設定ファイルに記述
3. {
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
//PostgreSQLへの接続先情報
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=postgres"
}
}
エントリーポイントでDbContextクラスのインスタンスを作成
4. using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// AddDbContextクラスを作成(appsettings.jsonで定義した接続先情報を用いて)
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")!));
// その他のサービス定義
var app = builder.Build();
// ミドルウェア定義
app.Run();
初期マイグレーションを作成
5. プロジェクトのルートディレクトリで以下のコマンドを実行して初期マイグレーションを作成します。
dotnet ef migrations add InitialCreate
データベースにマイグレーションを適用(データベースを更新)
6. 作成したマイグレーションをデータベースに反映させるには、以下のコマンドを実行します。
これにより、以下のように変更履歴およびテーブルが作成されます。
dotnet ef database update
成果物確認
7. マイグレーションにより、変更履歴のコード及びテーブルが作成されます。ロールバック処理やデータベース変更履歴のGit管理が可能となります。
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EFCoreTest.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Products",
columns: table => new
{
Id = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Products", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Products");
}
}
}
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace EFCoreTest.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Product", b =>
{
b.Property<string>("Id")
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.HasKey("Id");
b.ToTable("Products");
});
#pragma warning restore 612, 618
}
}
}
実際に作成されたProductsテーブル
実際に作成された__EFMigrationsHistoryテーブル
スキャフォールド
スキャフォールドでは、既存のデータベースから自動的にエンティティクラスやDbContextクラスを生成することができます。EF Core では、以下の手順でマイグレーションを実現できます。
いわゆる、データベースファーストの考え方です。業務要件としては、こちらを利用するパターンが多いのではないかと思います。
前提
- 接続先データベースに以下のDDLで作成されたProductsテーブルが存在すること
CREATE TABLE "Products" (
"Id" character varying(10) NOT NULL,
"Name" character varying(100) NOT NULL,
CONSTRAINT "PK_Products" PRIMARY KEY ("Id")
);
手順
スキャフォールドの実行
1. プロジェクトのルートディレクトリで以下のコマンドを実行してスキャフォールドを実行します。
dotnet ef dbcontext scaffold "Host=localhost;Database=postgres;Username=postgres;Password=postgres" Npgsql.EntityFrameworkCore.PostgreSQL --data-annotations --context-dir DBContext/ --output-dir Entities/ --force -v
本コマンドで使用しているコマンドオプションの説明は以下の通りです。
オプション | 説明 |
---|---|
Host |
データベースへの接続文字列。ここでは、PostgreSQLデータベースへの接続情報を指定しています。 |
Npgsql.EntityFrameworkCore.PostgreSQL |
使用するプロバイダーを指定します。この場合は、PostgreSQL用のEntity Framework Coreプロバイダーです。 |
--data-annotations |
データアノテーションを使用してモデルを生成します。これにより、データ制約や属性がエンティティクラスに追加されます。 |
--context-dir |
DbContextクラスを生成するディレクトリを指定します。この場合、DBContext/ に生成されます。 |
--output-dir |
エンティティクラスを生成するディレクトリを指定します。この場合、Entities/ にエンティティが生成されます。 |
--force |
既存のファイルを上書きするオプションです。これにより、同名のファイルが存在しても強制的に生成します。 |
-v |
詳細な出力を表示するオプションです。コマンドの実行中に発生した情報やエラーの詳細を表示します。 |
成果物確認
2. スキャフォールドを実行することで、以下のようにデータベースからDBContextクラスとEntityクラスが自動生成されることが確認できます。
既にデータベース環境がある場合はスキャフォールドを用いることで、EF Coreを利用する環境が迅速に構築できるので非常に便利な機能かと思います。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace EFCoreTest.Entities;
public partial class Product
{
[Key]
[StringLength(10)]
public string Id { get; set; } = null!;
[StringLength(100)]
public string Name { get; set; } = null!;
}
using System;
using System.Collections.Generic;
using EFCoreTest.Entities;
using Microsoft.EntityFrameworkCore;
namespace EFCoreTest.DBContext;
public partial class PostgresContext : DbContext
{
public PostgresContext()
{
}
public PostgresContext(DbContextOptions<PostgresContext> options)
: base(options)
{
}
public virtual DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
=> optionsBuilder.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=postgres");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasPostgresExtension("pg_catalog", "adminpack");
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
Ef Coreを用いたCRUD操作
前提
以下のように、エンティティおよびContextクラスを作成しておきます。
1. Productエンティティの定義
public class Product
{
[Key]
[StringLength(10)]
public string Id { get; set; } = null!;
[StringLength(100)]
public string Name { get; set; } = null!;
}
2. DbContextクラスの定義
using Microsoft.EntityFrameworkCore;
public partial class PostgresContext : DbContext
{
public PostgresContext(DbContextOptions<PostgresContext> options)
: base(options)
{
}
public virtual DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
CRUD操作
Productエンティティクラスを介して、Productテーブルを操作する例を以下に示します。
using Microsoft.EntityFrameworkCore;
public class ProductRepository
{
private readonly PostgresContext _context;
public ProductRepository(PostgresContext context)
{
_context = context;
}
// Create(作成)
public async Task Crate(Product product)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
}
// Read(読み込み)選択条件:主キー
public async Task<Product> FindById(string id)
{
return await _context.Products.SingleOrDefaultAsync(p => p.Id == id);
}
// Read(読み込み)選択条件:主キー以外
public async Task<List<Product>> FindByName(string name)
{
return await _context.Products.Where(p => p.Name == name).ToListAsync();
}
// Update(更新)
public async Task Update(Product product)
{
var target = await _context.Products.FirstOrDefaultAsync(p => p.Id == product.Id);
target.Name = product.Name;
await _context.SaveChangesAsync();
}
// Delete(削除)
public async Task Delete(Product product)
{
_context.Products.Remove(product);
await _context.SaveChangesAsync();
}
}
拡張LINQメソッド
EF Core では、拡張LINQメソッドを自作するのをよく見かけます。
ここでは、よく実装されるであろう条件付き WHERE 句(WhereIf)とページング処理の実装例を紹介します。
これらの拡張メソッドを利用することで、コード内で簡潔に扱うことができるかと思います。
拡張メソッド実装
using System.Linq.Expressions;
public static class LinqExtentions
{
/// <summary>
/// 動的にWHERE文を追加
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="condition">任意の条件式</param>
/// <param name="predicate">条件がTrueの場合に適用する選択条件</param>
/// <returns></returns>
public static IQueryable<T> WhereIf<T>(this IQueryable<T> query, bool condition, Expression<Func<T, bool>> predicate)
{
return condition ? query.Where(predicate) : query;
}
/// <summary>
/// ページング処理
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="pageNumber">ページ番号</param>
/// <param name="pageSize">ページサイズ</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static IQueryable<T> PageBy<T>(this IQueryable<T> query, int pageNumber, int pageSize)
{
return query.Skip((pageNumber - 1) * pageSize).Take(pageSize);
}
}
使い方の例
using Microsoft.EntityFrameworkCore;
public class ProductRepository
{
private readonly PostgresContext _context;
public ProductRepository(PostgresContext context)
{
_context = context;
}
// WhereIfの利用例
public async Task<List<Product>> FindByName(string name, bool flag)
{
// flagがtrueならnameに対して選択条件を付与する
return await _context.Products.WhereIf(flag, p => p.Name == name).ToListAsync();
}
// ページングの利用例
public async Task<List<Product>> GetAllByPagenation(int pageNumber, int pageSize)
{
// ページングを用いたデータ取得
return await _context.Products.PageBy(pageNumber, pageSize).ToListAsync();
}
}
まとめ
- Ef Coreを用いることで、開発速度の向上やコードの可読性/保守性の向上が期待できます。
ただし、LINQの扱いに慣れていない場合や複雑なSQLクエリを実装する場合は適さない場合があるかと思います。 - マイグレーション機能を用いることで、コードファーストでのデータベース作成およびデータベースの変更履歴管理が可能となります。
- スキャフォールド機能を用いることで、データベースファーストでのエンティティクラスやDBContextクラスを作成することが可能となります。
Discussion