WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

PlanetScale を使ってみました

by WebSurfer 15. February 2023 19:11

ASP.NET Core MVC アプリで PlanetScale をデーターベースに使ってみました。紆余曲折がありましたが何とか接続して使えるようになった結果が下の画像です。

PlanetScale を使ったアプリ

その紆余曲折と言うか、アプリの作成で自分が遭遇した問題は以下の通りです。

  1. EF Code First でテーブルを作成できない。
  2. 既存の DB からリバースエンジニアリングができない。
  3. BeginTransaction メソッドが使えない。

その顛末を以下に備忘録として書いておきます。上の各項目についても説明します。

PlanetScale とは何かですが、"PlanetScale is the world’s most advanced serverless MySQL platform" ということだそうで、要するに MySQL を使ったクラウド上のデータベースサーバーらしいです。制約として外部キー制約がつけられないということがあるそうです。

利用するにあたって、まず、PlanetScale にサインインしてデータベース名と地域を設定し、データベースを作成する必要があります。以下の画像がその設定の例です。

データベース名と地域を設定

データベースの作成に成功すると、以下のように自分が作成したデーターベース情報が表示されます。

自分の DB 情報

上の画像の[Connect]ボタンをクリックすると、下の画像のように接続情報が表示されます。Username と Password が表示されるのは初回のみなので必ず記録しておいてください

接続情報

さらに、上の画像の赤枠の[.NET]を選択すると ASP.NET Core アプリの appsettings.json に設定するための接続文字列の例が表示されますので、これも忘れないように記録しておくことをお勧めします。

以上はデータベース作成時からデフォルトで存在している main ブランチでの話です。このあと開発作業用にブランチを追加するのが普通ということなので自分もそうしてみました。気をつけなければならないのは追加した開発作業用ブランチへの接続用の Username と Password は main ブランチのものとは異なることです。

自分はそれを知らなくて半日ぐらいハマりました。開発作業用ブランチを作ってそれにテーブルを作成し、アプリからアクセスして SELECT クエリを投げたのですが、MySqlException がスローされてそのテーブルは見つからないと言われます。原因は Username と Password に main ブランチのものを使っていたので main ブランチに接続され、main ブランチにはそのテーブルは存在しないからでした。開発作業用ブランチに接続するには、開発作業用ブランチの Username と Password を使用する必要があるようです。

以上で PlanetScale 側のデータベースの準備は完了したはずなので、Visual Studio 2022 のテンプレートを使って .NET 6.0 の ASP.NET Core MVC のプロジェクトを新規に作成し、それから PlanetScale を使ってみます。

(1) EF Code First でテーブルを作成できない

最初に EF Code First の機能を使って PlanetScale にアプリが使うテーブルを作成してみます。先の記事「MySQL で Movie チュートリアル (CORE)」に書いた手順で、EF Code First の機能を利用して PlanetScale に Movie テーブルの作成をトライしてみました。

Add-Migration では問題なく Migration のためのクラスファイルが作成されます。しかし、Update-Database でのテーブルの作成に失敗します。

プロバイダに MySql.EntityFrameworkCore 6.0.10 を使った場合は、Update-Database の実行で KeyNotFoundException がスローされて失敗します。

Pomelo.EntityFrameworkCore.MySql 6.0.2 を使った場合は、ALTER DATABASE CHARACTER SET utf8mb4 というコマンドで失敗し "alter database is not supported" というエラーメッセージが出て、テーブルの作成に失敗します。

.NET 7.0 でも試してみましたが同じエラーで失敗します。

(2) リバースエンジニアリングができない

解決方法が分からないので、EF Code First による Movie テーブルの作成は諦めて、PlanetScale のサイトにアクセスして Console から手動で Movie テーブルを作成しました。下の画像の movie というのがそれです。

Console から手動で Movie テーブル作成

(注: __EFMigrationsHistory というテーブルもありますが、それは上に書いた EF Code First の操作でできたものです。その時は肝心の Movie テーブルは作成されていません)

先に作ったプロジェクトは放置して新たにゼロから ASP.NET Core MVC のプロジェクトを作成し、プロバイダには Pomelo.EntityFrameworkCore.MySql 6.0.2 を使って、リバースエンジニアリングでコンテキストクラスとエンティティクラスを作成をトライしました。

エラーなしで完了するもののコンテキストクラスしか生成されずその内容も不完全です。エンティティクラスは全く作成されません。

ちなみに、プロバイダに MySql.EntityFrameworkCore 6.0.10 を使った場合は KeyNotFoundException がスローされて何も生成されずに終わってしまいます。

これでは先に進めないので、不完全なコンテキストクラスは手直しして、エンティティクラスは自分でコードを書いて追加しました。その内容は以下の通りです。

コンテキストクラス

using Microsoft.EntityFrameworkCore;
using PlanetScaleMovie2.Models;

namespace PlanetScaleMovie2.Data
{
    public partial class myplanetscaleContext : DbContext
    {
        public myplanetscaleContext()
        {
        }

        public myplanetscaleContext(DbContextOptions<myplanetscaleContext> options)
            : base(options)
        {
        }

        // 以下の一行だけは自動生成されないので手動で追加
        public virtual DbSet<Movie> Movies { get; set; } = null!;

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            // 自動生成されたコードは不要なので削除
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.UseCollation("utf8mb4_0900_ai_ci")
                .HasCharSet("utf8mb4");

            OnModelCreatingPartial(modelBuilder);
        }

        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    }
}

エンティティクラス

using Microsoft.EntityFrameworkCore.Metadata.Internal;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;

namespace PlanetScaleMovie2.Models
{
    [Table("movie")]
    public partial class Movie
    {
        [Key]
        public int Id { get; set; }
        [Column(TypeName = "text")]
        public string? Title { get; set; }
        [Column(TypeName = "datetime")]
        public DateTime ReleaseDate { get; set; }
        [Column(TypeName = "text")]
        public string? Genre { get; set; }
        public decimal Price { get; set; }
    }
}

そのあとスキャフォールディング機能を使って Controller と View を作成します。

Controller と View の作成

Program.cs でコンテキストクラスを DI コンテナに登録します。

var serverVersion = new MySqlServerVersion(new Version(8, 0, 32));
var coonecctionString = builder.Configuration.GetConnectionString("PlanetScaleMovieContext");
builder.Services.AddDbContext<myplanetscaleContext>(options =>
    options.UseMySql(coonecctionString, serverVersion));

appsettings.json に上のコードの "PlanetScaleMovieContext" という名前で接続文字列を追加します。Movie テーブル を開発作業用ブランチに作った場合は、接続文字列の user と password は開発作業用ブランチ用のものにすることに注意してください。

以上でアプリは今度は問題なく動きました。その結果がこの記事の一番上の画像です。

(3) BeginTransaction メソッドが使えない

これはプロバイダに Oracle 製の MySql.EntityFrameworkCore を使った場合です。以下のように BeginTransaction メソッドの行で KeyNotFoundException がスローされます。

BeginTransaction メソッド

プロバイダに Pomelo.EntityFrameworkCore.MySql を使った場合は問題ありません。Oracle 製はどうも相性が良くないようです。

なお、上記 (1), (2) の件については planetscale / discussionDoes PlanetScale support the .NET 6/7 EF Code First and reverse engineering? という質問を出しました。解決に向けて進展がありましたらこの記事に追記します。

Tags: , , ,

CORE

SelectMany メソッド

by WebSurfer 21. March 2022 19:15

Linq の SelectMany メソッドについて調べて、多少なりとも分かったような気になったので、自分なりの理解を備忘録として書いておくことにしました。

顧客が過去に注文した製品一覧

自分の手を動かしてコードを書くと理解が深まるだろうと思って、Northwind サンプルデータベースの Customers テーブルの顧客が過去に注文した製品の一覧を SelectMany メソッドと GroupBy メソッドを使って取得するサンプルを作ってみました。上の画像がその結果を表示したものです。どのようなコードを書いたかは後述します。

Microsoft のドキュメント「Enumerable.SelectMany メソッド」を見ると "シーケンスの各要素を IEnumerable<T> に射影し、結果のシーケンスを 1 つのシーケンスに平坦化します。 Projects each element of a sequence to an IEnumerable<T> and flattens the resulting sequences into one sequence." と書いてあるのですが、自分の頭ではその説明ではさっぱり意味が分かりませんでした。「シーケンス (sequence)」って何? 「射影 (project)」って何? 「平坦化 (flatten)」ってどういうこと?・・・って感じ。(汗)

ググって調べてみると「シーケンス」というのは .NET の Linq の世界に限れば "IEnumerable または IEnumerable<T> インターフェイスを継承するオブジェクト" と理解すれば良さそうです。

「射影」というのは Microsoft のドキュメント「射影操作 (C#)」によれば "オブジェクトを必要なプロパティだけで構成された別の形式に変換する操作" ということだそうです。その際「平坦化」を同時に行うのが Select メソッドとは違う所のようです。

で、問題の「平坦化」ですが、これは BuildInsider の記事「LINQ:取得列を明示的に指定する - select句/SelectManyメソッド[C#]」の説明が分かりやすかったです。

下の画像は Northwind サンプルデータベースの Customers, Orders, Order_Details, Products テーブルから生成した Entity Data Model ですが、これを例に取って説明します。

Northwind EDM

Orders の中には複数の顧客の注文データが複数(過去の注文の数)含まれており、各注文に紐づく詳細は Order_Details に含まれています。Order_Details のデータは Orders のナビゲーションプロパティ Order_Details から取得できます。

Orders から CustomerID が "ALFKI" の顧客の注文(Orders の中に複数あります)を抽出し、それに紐づく Order_Details を Select および SelectMany メソッドで引数にナビゲーションプロパティ Order_Details 設定して取得してみます。

Select メソッド

結果のオブジェクトが List<ICollection<OrderDetail>> 型となっています。上に紹介した BuildInsider の記事にも書いてありますように、OrderDetail にアクセスするためには 2 回ループを回す必要があります。

Select の結果

SelectMany メソッド

結果のオブジェクトが List<OrderDetail> 型になっており「平坦化」されているのが分かるでしょうか?

SelectMany の結果

ちなみに SelectMany メソッドの引数に指定するナビゲーションプロパティは IEnumerable<T> 型でなければならないので注意してください。間違って他の Employee 型とかのプロパティを設定すると以下のようなエラーが出ます。(意味不明なので悩むかも。何を隠そう自分がそうでした)

"エラー CS0411 メソッド 'Enumerable.SelectMany<TSource, TResult>(IEnumerable<TSource>, Func<TSource, IEnumerable<TResult>>)' の型引数を使い方から推論することはできません。型引数を明示的に指定してください。"


もう一つ、上の例より実用的かもしれないサンプルコードを載せておきます。 ASP.NET Core MVC アプリで、Customers テーブル顧客一覧を表示し (Customers/Index)、一覧の中から選んだ特定の顧客が過去に注文した製品の一覧を SelectMany メソッドと GroupBy メソッドを使って取得し、ViewData を使って View に渡して表示するもので (Customers/Details)、この記事の一番上の画像がその結果です。

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MvcCore6App2.Data;

namespace MvcCore6App2.Controllers
{
    public class CustomersController : Controller
    {
        private readonly NorthwindContext _context;

        public CustomersController(NorthwindContext context)
        {
            _context = context;
        }

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

        public async Task<IActionResult> Details(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var customer = await _context.Customers
                .Include(c => c.Orders)
                    .ThenInclude(o => o.OrderDetails)
                        .ThenInclude(od => od.Product)
                .FirstOrDefaultAsync(c => c.CustomerId == id);

            var orderDetails = customer?.Orders
                .SelectMany(o => o.OrderDetails);

            if (orderDetails != null)
            {
                ViewData["PastOrderedProducts"] = orderDetails
                    .GroupBy(od => od.Product)
                    .Select(g => new PastOrderedProducts
                    { 
                        ProductId = g.Key.ProductId,
                        ProductName = g.Key.ProductName,
                        Quantity = g.Sum(g => g.Quantity) 
                    }).ToList();
            }

            if (customer == null)
            {
                return NotFound();
            }

            return View(customer);
        }
    }

    public class PastOrderedProducts
    {
        public int ProductId { get; set; }
        public string? ProductName { get; set; }
        public int Quantity { get; set; }
    }
}

Tags: , ,

ADO.NET

Entity Framework で ROW_NUMBER

by WebSurfer 20. September 2021 15:27

SQL Server のテーブルからレコードを抽出する際、あるフィールドに ORDER BY 句を適用して並べ替え、その順序で連番を振りたいという場合は ROW_NUMBER (Transact-SQL) を使うことができます。

ROW_NUMBER の使用

上の画像がその例で、ProductName フィールドに ORDER BY 句を適用して昇順に並べ替えて、ROW_NUMBER を使ってその順で 1 から連番を振って、その連番を SeqNum という名前で取得しています。期待通り SeqNum に連番が取得されているのが分かるでしょうか?

それを同じことを Entity Framework ではどのようしたらできるかというのがこの記事の話です。

この記事を書いた時点で自分が調べた限りですが、ROW_NUMBER は Entity Framework ではサポートされてないようで(NuGet から Thinktecture.EntityFrameworkCore.SqlServer をインストールして使えるようにするという手はあるようですが)、Linq to Entities / Objects のクエリ式やメソッド式に ROW_NUMBER を含めることはできないようです。

Microsoft のドキュメント「生 SQL クエリ」に紹介されているように、FromSqlRaw 拡張メソッドを使用して上の画像の SELECT クエリをそのまま SQL Server に投げるという手も考えましたが、戻ってきた結果から SeqNum を取得する方法が見つかりません。(自分が見つけられないだけで手はあるのかもしれませんが)

ではどうするかですが、ググって調べていると Generating Sequence Numbers In LINQ Query という記事を見つけました。

IEnumerable<T> インターフェイスの Select 拡張メソッドのオーバーロードの一つに Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,Int32,TResult>) があって、それを使えば 0 番から始まる連番の index を取得することができるというものです。

その記事を参考に、上の画像の ROW_NUMBER を使った SELECT クエリと同じ結果を Linq to Entities / Objects を使って取得するコードを書いてみました。以下の通りです。

public List<ProductWithSeqNum> GetListWithSeqNum()
{
    var data = _context.Products
               .Select(a => new 
               {
                   ProductId = a.ProductId,
                   ProductName = a.ProductName,
                   UnitPrice = a.UnitPrice.Value
               });

    var list = data.AsEnumerable()
               .OrderBy(a => a.ProductName)
               .Select((a, index) => new ProductWithSeqNum
               {
                   SeqNum = index + 1,
                   ProductId = a.ProductId,
                   ProductName = a.ProductName,
                   UnitPrice = a.UnitPrice
               });

    return list.ToList();
}

まず、Linq to Entities を使って Products テーブルから ProductId, ProductName, UnitPrice 抽出して IQueryable<匿名クラス> のオブジェクトを取得し、変数 data に格納しています。

次に、data.AsEnumerable() で IEnumerable<匿名クラス> に変換して、それを OrderBy メソッドで ProductName 順に並べ替え、上に述べた Select メソッドの index を使って 1 から始まる連番を SeqNum に格納しています。

ProductWithSeqNum クラスを Data Transfer Object (DTO) として使っていて、結果を List<ProductWithSeqNum> オブジェクトとして戻しています。ProductWithSeqNum クラスの定義は以下の通りです。

public class ProductWithSeqNum
{
    public int SeqNum { get; set; }

    public int ProductId { get; set; }

    public string ProductName { get; set; }

    public decimal UnitPrice { get; set; }
}

上の GetListWithSeqNum メソッドで取得した結果をコンソールに書き出すと以下の通りとなります。一番上の画像と同様な結果が取得できているのが分かるでしょうか。

結果をコンソールに書き出し

Tags: , ,

ADO.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  June 2023  >>
MoTuWeThFrSaSu
2930311234
567891011
12131415161718
19202122232425
262728293012
3456789

View posts in large calendar