最近Entity Frameworkを知りました。いまさらです。生きててすみません。
聞いたことはありましたが、いままでのやり方に固執・執着・思考停止(※ほんと良くないですよね・・・)し、あまり気にもとめてなかったのですが、実際いろいろやってみたら「あーこれは楽だ。きっと実務がはかどる。」とちょっと感動を覚えましたので投稿したいと思います。
それにしてもEntityFramework、.NET Framework 3.5 SP1の頃から実装されていたなんてちょっとビックリしました。初期の頃は今の使い勝手とはだいぶ違うかもしれませんが、もったいないことしたなぁと・・・。まぁ過去を悔やんでても仕方ないので前向いていきましょう。
はじめに
本日のゴール
Azure FunctionsでEFCoreのコードファースト開発を行う最初の一歩を実装します。
EntityFrameworkのより深く広い機能やコードファーストでリレーショナルなモデルを構築するような部分には触れてません。
以下の動画が参考になるかもしれません。英語ですが。コード読めれば大丈夫かと。
環境とか
開発環境:Microsoft Visual Studio 2022 Community
データベース:Microsoft SQLServer 2019 Express
Azure Functionsランタイム:.NET 6(LTS)
実装
新規Azure Functionsプロジェクトの作成
①まずはVisualStudioを起動し、Azure Functionsを新規プロジェクトとして作成していきます。「新しいプロジェクトの作成」を選択します。
②テンプレートとして「Azure Functions」を選択します。
③プロジェクト名や保存場所を指定したら「作成」ボタンを押下します。
④ランタイムとして「.NET 6.0(LTS)」を選択し、トリガーは「Http trigger」を選択します。「作成」ボタンを押下しましょう。Azure Functionsのテンプレコードが作成されると思います。
NuGetパッケージマネージャーからパッケージをインストール
EntityFrameworkCore(+DI(依存性注入))を利用する上で必要となるパッケージをインストールします。
必要なパッケージは以下の通りです。
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
- Microsoft.Azure.Functions.Extensions
①「ツール」メニューから「NuGetパッケージマネージャー」-「ソリューションのNuGetパッケージの管理」を選択します。
②参照タブで「EntityFramework」と検索し、表示された「Microsoft.EntityFrameworkCore」パッケージをプロジェクトにインストールします。この時、Azure Functionsのランタイムバージョンが.NET6ならば、バージョンを「6.X.X」に指定するのを忘れないようにしましょう。「インストール」ボタンを押下します。
②同様に「Microsoft.EntityFrameworkCore.Design」、「Microsoft.EntityFrameworkCore.SqlServer」、「Microsoft.EntityFrameworkCore.Tools」をバージョンを考慮してインストールします。「Microsoft.Azure.Functions.Extensions」は最新版のインストールでOKです。
EntityFrameworkCoreのコードファースト用コードを追加
次に対象のテーブルとマップするエンティティクラスを用意していきます。今回は簡単な例として作るので、IdフィールドとNameフィールドを保持するUserテーブルが出来上がるようにコーディングしていきます。
①まずはエンティティをフォルダーに整理したいのでModelsフォルダーを追加します。プロジェクトを右クリックし、「追加」-「新しいフォルダー」を選択しましょう。
Modelsフォルダーが追加されました。
②Modelsフォルダー内にUserエンティティクラスを追加します。Modelsフォルダーを右クリックし、「追加」-「新しい項目」を選択します。
③「クラス」を選択し、名前に「User.cs」を指定して、「追加」ボタンを押下します。
④追加したUser.csを以下のように実装します。ちなみにIdというフィールド名を指定すると自動的にオートナンバーのプライマリーキーで構築されます。自動採番したくない場合は、DatabaseGenerated属性のDatabaseGeneratedOption.Noneを指定することで実現可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeFirstExampleFunctionApp.Models { [Table("User")] public class User { public int Id { get; set; } public string Name { get; set; } } } |
⑤同様にDbContextクラスもModelsフォルダー内に追加しましょう。こちらも「クラス」を選択し、名前は任意の名前を指定して、「追加」ボタンを押下します。今回は「MyContext.cs」とします。名前を「DbContext.cs」にはしないほうがいいと思います。多分だめです。
⑦追加したMyContextクラスもコードを記述します。基本的にはDbContextを継承したクラスを実装していく感じです。DbSet型はコレクションなので複数形にするのがいいようです。ただ、前項のUserオブジェクトで[Table]属性を省略した場合はおそらく実テーブルもUsersと複数形で構築されると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; namespace CodeFirstExampleFunctionApp.Models { public class MyContext : DbContext { public MyContext(DbContextOptions options) : base(options) { } public DbSet<User> Users { get; set; } } } |
コンテキストのファクトリークラスコードの追加
DbContextインスタンスを生成してくれるファクトリークラスコードを追加します。なぜ必要かなんですが、どうもこのファクトリークラスを経由してDbCotnextオブジェクトを生成しないと、後述のコマンド実行時に接続文字列を引き渡すことができなかったからです。
①場所はどこでもいいと思いますが、今回はModelsフォルダー内に「MyContextFactory.cs」の名前でクラスを追加します。
②追加したクラスに以下のコードを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; namespace CodeFirstExampleFunctionApp.Models { public class MyContextFactory : IDesignTimeDbContextFactory<MyContext> { public MyContext CreateDbContext(string[] args) { var optionsBuilder = new DbContextOptionsBuilder<MyContext>(); string connectionString = Environment.GetEnvironmentVariable("SqldbConnection"); optionsBuilder.UseSqlServer(connectionString); return new MyContext(optionsBuilder.Options); } } } |
DIパターンの実装
EntityFrameworkCoreでDbContextを扱うときはDI(依存オブジェクト注入)パターンを使うのがベストです。DIを実装していきます。
①プロジェクトを右クリックし、「追加」-「新しい項目」を選択します。
②「クラス」を選択し、名前を「Startup.cs」とし、「追加」ボタンを押下します。
③追加したStartup.csにコードを記述します。以下の通りにコードを追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using CodeFirstExampleFunctionApp.Models; [assembly: FunctionsStartup(typeof(CodeFirstExampleFunctionApp.Startup))] namespace CodeFirstExampleFunctionApp { public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { string connectionString = Environment.GetEnvironmentVariable("SqldbConnection"); builder.Services.AddDbContext<MyContext>(options => options.UseSqlServer(connectionString)); } } } |
local.settings.jsonにSQLServer接続文字列の追加
local.settings.json、つまり、環境変数にSQLServerの接続文字列を追加しておきます。前項で追加したStartup.cs内で環境変数から接続文字列の読み取りが行われ、DbContextに渡される流れです。
①local.settings.jsonに以下のコードを追加します。サーバー名やデータベース名は自信の環境に合わせ読み替えてください。
1 |
"SqldbConnection": "Server=localhost\\SQLEXPRESS;Database=ExampleDB;Trusted_Connection=True;" |
接続文字列追加後のlocal.settings.jsonは以下のような感じになるかと思います。
1 2 3 4 5 6 7 8 |
{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", "FUNCTIONS_WORKER_RUNTIME": "dotnet", "SqldbConnection": "Server=localhost\\SQLEXPRESS;Database=ExampleDB;Trusted_Connection=True;" } } |
マイグレーションの実行
マイグレーション、日本語に訳すと、移行と言ったりしますが、マイグレーションを行うことでその時点でのデータベースへの変更内容をマイグレーションファイルに記録しておき、随時データベースに反映させることができます。
まずはマイグレーションファイルの追加を行っていきます。
①「ツール」メニューの「NuGetパッケージマネージャー」から今度は「パッケージマネージャーコンソール」を起動します。
②パッケージマネージャーコンソールウインドウが表示されるので、まずは環境変数に接続文字列をセットします。「$env:SqldbConnection=”(SQLServer接続文字列)”」と入力し、Enterキーを押下します。
③次に、「add-migration (任意のバージョン管理名)」と入力し、Enterキーを押下しましょう。今回は初回の移行なので、「add-migration InitialCreate」としています。
ちなみに、コンテキストクラスの生成にファクトリークラスを追加して行いましたが、local.settings.jsonに設定した接続文字列はあくまで、Azure Functions実行時にロードされるが、マイグレーションコマンド実行時には別の方法で渡す必要があるのかなぁという感じです。ファクトリークラスを経由しないでadd-migrationを実行すると以下のエラーメッセージが表示されます。
リンク先を見ると、「デザイン時ファクトリーを利用して」とあるのでひとまず他のやり方はおいておいてファクトリークラス経由でコンテキストクラスオブジェクトを生成するのが無難そうです。
④うまくマイグレーションが成功すると、以下の画面のようになり、Migrationsフォルダーとファイルが追加されると思います。
⑤マイグレーションファイルができたら、その内容をもとにデータベースに変更を反映させます。パッケージマネージャーコンソールから「update-database」と入力し、Enterキーを押下しましょう。
以下の画面のように表示され、最後にDoneとなっていたらデータベースへの反映は完了です。
⑥SSMSからデータベース内容を確認すると、データベースとUserテーブルが作成され、IDフィールドとNameフィールドが存在するのが確認できると思います。
Azure FunctionsからEFCoreを利用してDBアクセス
EFCoreのコードファースト、初回構築はうまくいきました。ひとまずコードファーストとしてはここまでですが、最後にAzure FunctionsからUserテーブルに書き込みができるかを確認してみます。
①まずはテンプレで作成されたAzureFunctionsコードをDIでDbContextを受け取れるように修正します。また、URLパラメーターで受け取ったNameパラメーターでUserテーブルに追加するようにし、登録内容(自動採番されたIDとユーザー名)をブラウザーに返すように実装します。以下、そのコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
using System; using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using CodeFirstExampleFunctionApp.Models; namespace CodeFirstExampleFunctionApp { public class Function1 { private readonly MyContext dbContext; public Function1(MyContext context) { dbContext = context; } [FunctionName("Function1")] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log) { log.LogInformation("C# HTTP trigger function processed a request."); string name = req.Query["name"]; await dbContext.AddAsync(new User() { Name = name }); await dbContext.SaveChangesAsync(); // 自動採番された最後のIDを取得する int id = dbContext.Users.Max(x => x.Id); return new OkObjectResult($"ユーザーを登録しました。ID={id}、ユーザー名={name}"); } } } |
②Azure Functionsをデバッグ実行します。表示されたURLをコピーしましょう。
③コピーしたURLをブラウザーのアドレスバーに貼り付け、後ろに「?name=(任意の名前)」を付加し、Enterキーを押下します。
④正常にデータベースへ更新されれば以下のようにレスポンスメッセージが返ってくると思います。
SSMSでUserテーブルの中を確認すると、ちゃんとIDが自動採番されレコードが登録されたのではないでしょうか。
以上、Azure FunctionsでもEntityFrameworkCoreのコードファーストでがっつり開発できそうなことが確認できました。
コメント