Babelfish for Aurora PostgreSQL で ASP.NET Core + EF Core のコードファーストアプローチが使えるか確認してみた

2022.08.05

いわさです。

Babelfish for Aurora PostgreSQL は、Aurora PostgreSQL に TDS で接続出来るように機能で、Microsoft SQL Server 向けに作成されているアプリケーションのデータベースを Amazon Aurora へ移行させる際に利用することが出来ます。

ただし、SQL Server の全ての機能がサポートされているわけではないため、どのワークロードでも Amazon Aurora へ移行出来るわけではありません。

これまでは、Babelfish for Aurora に対してコンソールの SQL クライアントや SSMS でアクセスを行ってきました。 その際に、SSMS のGUI上では一部サポートされていない機能(データベース作成など)も確認することが出来ました。

.NET アプリケーションを構築する場合、ORM として Entity Framework Core (EF Core) を採用することが多いと思います。 今回は、EF Core で Babelfish が問題なく扱えるのかを確認してみました。

EF Core と ASP.NET Core

Babelfish の準備

まずは Babelfish を準備します。

今回は本日時点で最新の Aurora PostgreSQL 14.3 を使います。 Aurora PostgreSQL のバージョンに連動して組み込まれている Babelfish のバージョンも新しいものになり、制限事項が緩和されていたりするので、Babelfish を使う上では出来るだけ新しい Aurora PostgreSQL バージョンを選択することが良いと思います。

ASP.NET Core MVC + EF Core アプリケーションの準備

次に、Babelfish へデータアクセスを行う Web アプリケーションを用意します。 本記事とは関係のない要件で MVC で作成する必要があるので、dotnet new mvcから開始しています。

そして、EF Core では様々なデータベースプロバイダーを使うことが出来ます。

今回は SQL Sever へアクセスする想定なので、Microsoft.EntityFrameworkCore.SqlServer:6.0.7を採用したいと思います。

% dotnet new mvc
テンプレート "ASP.NET Core Web App (Model-View-Controller)" が正常に作成されました。
このテンプレートには、Microsoft 以外のパーティのテクノロジが含まれています。詳しくは、https://aka.ms/aspnetcore/6.0-third-party-notices をご覧ください。

作成後の操作を処理しています...
/Users/iwasa.takahito/src/sample-dotnet-mvc-efcore/sample-dotnet-mvc-efcore.csproj で ' dotnet restore ' を実行しています...
  Determining projects to restore...
  Restored /Users/iwasa.takahito/src/sample-dotnet-mvc-efcore/sample-dotnet-mvc-efcore.csproj (in 92 ms).
正常に復元されました。

% dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 6.0.7
  Determining projects to restore...
  Writing /var/folders/4d/nhd1bp3d161crsn900wjrprm0000gp/T/tmp9XWVxp.tmp
info : Adding PackageReference for package 'Microsoft.EntityFrameworkCore.SqlServer' into project '/Users/iwasa.takahito/src/sample-dotnet-mvc-efcore/sample-dotnet-mvc-efcore.csproj'.
:

あとは以下を参考に Web アプリケーションに EF Core に関する実装を行っていきます。 なお、今回はコードファーストアプローチを採用しています。

完全なコードは記事のさいごに GitHub リポジトリへのリンクを貼っておきます。 記事内でポイントを抜粋して紹介します。

最低限な感じですが、以下のようにDbContextFugaクラスを用意しています。 Aurora 上に作成されるテーブルをコード化するイメージです。

Data/Hoge1Context.cs

using Microsoft.EntityFrameworkCore;

namespace sample_dotnet_mvc_efcore.Data
{
    public class Hoge1Context : DbContext
    {
        public Hoge1Context(DbContextOptions<Hoge1Context> options) : base(options)
        {
        }

        public DbSet<Fuga> Fugas { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Fuga>().ToTable("Fuga");
        }
    }

    public class Fuga
    {
        public int FugaId { get; set; }

        public string FugaName { get; set; } = string.Empty;
    }
}

スタートアップ処理で作成した DbContext サービスの追加と、データベースの初期化(テーブル作成、初期データ作成)処理を実装しています。

Program.cs

using Microsoft.EntityFrameworkCore;
using sample_dotnet_mvc_efcore.Data;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<Hoge1Context>(options => 
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("HogeContext"));
});

:

using(var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var context = services.GetRequiredService<Hoge1Context>();
    context.Database.EnsureCreated();

    //Initialize
    if(context.Fugas.Any())
    {
        return;
    }
    var fugas = new Fuga[]
    {
        new Fuga { FugaName = "aaa"},
        new Fuga { FugaName = "bbb"},
        new Fuga { FugaName = "ccc"},
    };
    context.Fugas.AddRange(fugas);
    context.SaveChanges();
}

app.UseHttpsRedirection();

:

appsettings.json にてデータベースへの接続情報を設定します。 なお、後述しますが、対象の Aurora 上にはInitial Catalogで指定している名称で空のデータベースが存在している必要があります。

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "HogeContext": "Data Source=hoge0805multi.cluster-cpnu9ipu74g4.ap-northeast-1.rds.amazonaws.com,1433;Initial Catalog=hoge1;User ID=postgres;Password=hogehoge"
  }
}

あとはデータベースを利用する部分だけ適当なコントローラーとビューに実装します。

Controllers/HomeController.cs

using sample_dotnet_mvc_efcore.Data;
using sample_dotnet_mvc_efcore.Models;
:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly Hoge1Context _context;

    public HomeController(Hoge1Context context, ILogger<HomeController> logger)
    {
        _logger = logger;
        _context = context;
    }

    public async Task<IActionResult> Index()
    {
        return View(await _context.Fugas.ToListAsync());
    }

:
}

Views/Home/Index.cshtml

@model IEnumerable<sample_dotnet_mvc_efcore.Data.Fuga>
@{
    ViewData["Title"] = "Home Page";
}


<div class="text-center">
    
<h1 class="display-4">Welcome</h1>

    @* 

Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.

 *@
    
<table class="table">
        
<tbody>
@foreach (var item in Model){
            
<tr>
                
<td>@Html.DisplayFor(modelItem => item.FugaId)</td>

                
<td>@Html.DisplayFor(modelItem => item.FugaName)</td>

            </tr>

            
}
        </tbody>

    </table>

</div>

今回は RDS にパブリックアクセス可能な状態で作成しているため、dotnet runでローカル実行してみます。

初期化処理でテーブルとレコードが作成され、ページを表示した際にデータベース登録内容が取得されていることが確認出来ました。 EF Core でも問題なく利用出来ることがわかりました。

DB作成は事前に行う必要がある

Program.csに実装したEnsureCreatedでは、データベースやテーブルが存在しない場合に作成を行ってくれます。 データベースが存在しない場合はデータベースから、データベースが存在してテーブルが存在しない場合はテーブルから作成してくれます。

しかし Babelfish を使う場合、データベースから作成しようとすると失敗します。 ただし、テーブルなしの空のデータベースが存在する場合であれば成功します。

% dotnet run
ビルドしています...
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 6.0.7 initialized 'Hoge1Context' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.7' with options: None
Unhandled exception. Microsoft.Data.SqlClient.SqlException (0x80131904): database "hoge1" does not exist
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)

:

   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabaseCreator.EnsureCreated()
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureCreated()
   at Program.<Main>$(String[] args) in /Users/iwasa.takahito/src/sample-dotnet-mvc-efcore/Program.cs:line 27
ClientConnectionId:4cc1f54f-4869-486d-a90a-24be360f91e7
Error Number:3701,State:1,Class:11

ちなみに実装が悪いわけではなく、以下のように Amazon RDS for SQL Server であれば問題なく動作します。

% dotnet run
ビルドしています...
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 6.0.7 initialized 'Hoge1Context' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.7' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (554ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      CREATE DATABASE [hoge1];
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (149ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      IF SERVERPROPERTY('EngineEdition') <> 5
      BEGIN
          ALTER DATABASE [hoge1] SET READ_COMMITTED_SNAPSHOT ON;
      END;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (42ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT 1
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (39ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [Fuga] (
          [FugaId] int NOT NULL IDENTITY,
          [FugaName] nvarchar(max) NOT NULL,
          CONSTRAINT [PK_Fuga] PRIMARY KEY ([FugaId])
      );
:

Amazon RDS for SQL Server の成功ログを見ると、ALTER DATABASE SETを行っていることがわかります。 実は Aurora PostgreSQL 14.3 がサポートする Babelfish 2.1.0 ではこの機能はサポートされていません。このせいで失敗しているようですね。

さいごに

本日は、Babelfish for Aurora PostgreSQL で ASP.NET Core + EF Core のコードファーストアプローチが使えるか確認してみました。

結果としては、ワークロードが通常データソースとして利用する分には問題なく使えそうです。 ただし、マイグレーションでデータベースから作成しようとすると Babelfish では失敗するので、予め空のデータベースの作成までは済ませておく必要がある点にご注意ください。