WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

null 許容許容参照型と EF Code First

by WebSurfer 12. May 2022 15:15

Visual Studio 2022 のテンプレートを使って .NET 6.0 アプリのプロジェクトを作ると、デフォルトで「Null 許容」オプションが有効にされています。その状態での EF Code First による SQL Server データベース生成に新発見 (自分が知らなかっただけですが) があったので備忘録として書いておきます。

「Null 許容」オプション

EF Code First でデータベースを生成すると、元となるコードのクラス定義の中のプロパティの型と付与する属性によって生成されるデータベースの列の型と NULL 可/不可が決まってきます。新発見というのは「Null 許容」オプションの有効化によって、生成される列の NULL 可/不可が以前と違ってくるということです。

値型の場合は「Null 許容」オプションの有効/無効は関係なく結果は以前と同じになります。すなわちデフォルトでデータベースの当該列は NULL 不可になります。NULL 可にしたい場合は Nullable<T> 型(例えば int? とか DateTime? など)をプロパティの型に使います。

違うのは参照型の場合です。「Null 許容」オプションが有効にされていると、例えばプロパティの型を string とすると当該データベースの列は NULL 不可に、string? とすると NULL 可になります。

以前 (null 許容参照型が使えない時代または「Null 許容」オプションが無効) は string 型は NULL 可になりました。NULL 不可にしたい場合は当該プロパティに RequiredAttribute 属性を付与していました。

実際にアプリを作って試してみましたので具体例を以下に書きます。

Visual Studio 2022 のテンプレートでフレームワークを .NET 6.0 としてコンソールアプリを作成します。その状態で上の画像のように「Null 許容」オプションが有効化されています。

NuGet パッケージ Microsoft.EntityFrameworkCore.SqlServer と Microsoft.EntityFrameworkCore.Tool をインストールします。前者は SQL Server 用の EF Core 本体、後者は Migration 操作を行うためのツールです。

NuGet パッケージ

Microsoft のドキュメント「新しいデータベースの Code First」と同様なコンテキストクラスとエンティティクラスを実装します。コードは以下の通りです。(null 許容参照型対応のため = null! を追加するなどしていますが基本は同じ)

public class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; } = null!;

    public virtual List<Post>? Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; } = null!;

    public string? Content { get; set; }

    public int BlogId { get; set; }
    public virtual Blog? Blog { get; set; }
}

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; } = null!;
    public DbSet<Post> Posts { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseSqlServer("接続文字列");
        }
    }
}

上のコードの Blog クラスの Name プロパティ、Post クラスの Title, Content プロパティの型を string / string? と使い分けている点に注目してください。これをベースに Migration 操作によって SQL Server にデータベースを生成した結果が以下の画像です。

SQL Server にデータベース生成

プロパティが string 型になっている Name, Title プロパティに対応する SQL Server データベースの Name 列と Title 列は NULL 不可に、string? 型になっている Content プロパティに対応するデータベースの Content 列は NULL 可になっています。

以前 (null 許容参照型が使えない時代または「Null 許容」オプションが無効) は、上にも書きましたが、プロパティの型が参照型の場合はデータベースの当該列は NULL 可になります。NULL 不可にする場合は RequiredAttribute 属性を付与します。試しに、以下のように #nullable disable を付与したクラス定義を追加し、Migration 操作で SQL Server に Products テーブルを生成してみました。

#nullable disable
public class Product
{
    public int ProductId { get; set; }

    [Required]
    public string ProductName { get; set; }

    public string Decription { get; set; }

    [Column(TypeName = "decimal(18,4)")]
    public decimal UnitPrice { get; set; }
}

結果は以下の通りです。プロパティの型が string の ProductName, Description に該当する列の NULL 可/不可を見てください。プロパティに RequiredAttribute 属性を付与しないと NULL 可になります。

#nullable disable で生成

既存のデータベースからリバースエンジニアリングで生成したコンテキストクラス、エンティティクラスも「Null 許容」オプションが有効化されている場合は上の Blog, Post クラスと同様になります。

試しに Microsoft のサンプルデータベース Northwind の Categories テーブルからリバースエンジニアリングでコンテキストクラス、エンティティクラスを生成してみました。Categories テーブルは以下の内容になっています。CategoryName 列が NULL 不可、Description 列が NULL 可になっているところに注目してください。

Northwind の Categories テーブル

上の Categories テーブルからリバースエンジニアリングを使ってデータアノテーション属性を含めてエンティティクラスを生成すると以下の通りとなります。データベースのテーブルの各列の NULL 可/不可と、生成されたクラス定義の各プロパティの型を見てください。データベースの列が NULL 可の場合はプロパティの型は null 許容(? を付与されている)となっています。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace MvcCore6App2.Models
{
    [Index("CategoryName", Name = "CategoryName")]
    public partial class Category
    {
        public Category()
        {
            Products = new HashSet<Product>();
        }

        [Key]
        [Column("CategoryID")]
        public int CategoryId { get; set; }
        [StringLength(15)]
        public string CategoryName { get; set; } = null!;
        [Column(TypeName = "ntext")]
        public string? Description { get; set; }
        [Column(TypeName = "image")]
        public byte[]? Picture { get; set; }

        [InverseProperty("Category")]
        public virtual ICollection<Product> Products { get; set; }
    }
}

この記事の本題の話は以上ですが、このクラス/プロパティ定義をそのまま ASP.NET MVC の Model として使った場合、ユーザー入力の検証がどうなるのかが気になります。それも調べましたので以下に書いておきます。

以前は string 型のプロパティに該当するテキストボックスへのユーザー入力を必須とする場合、その項目の当該プロパティに RequiredAttribute 属性を付与していました。それにより未入力の場合は検証 NG となってエラーメッセージが表示されます。

「Null 許容」オプションが有効化されている場合、string 型の項目は必須入力になるはずですが、上のコードではプロパティには RequiredAttribute 属性は付与されていません。そこはどうなるのかが疑問でした。

実際にアプリを動かして試してみると、RequiredAttribute 属性は付与されてなくても、未入力の場合は検証 NG となってエラーメッセージが表示されました。

View から生成される html ソースを調べてみると、当該 input 要素には data-val-required="The xxx field is required." という検証属性が付与され、未入力の場合は検証機能が働いてエラーメッセージが表示されるようになっていました。

エラーメッセージを任意のものに変えたい場合は、プロパティに RequiredAttribute を付与して ErrorMessage にメッセージを設定します。そうすると data-val-required 属性に設定される文字列が ErrorMessage に設定したものに置き換わります。

このあたりは先の記事「int 型プロパティの検証、エラーメッセージ」に書いた int 型の場合と同じになっているようです。

最後にもう一つ、こんなことをする人はいないかもしれませんが、string? 型のプロパティ(null 可)に RequiredAttribute 属性を付与(null 不可)するとどうなるかを書いておきます。

そのような設定をすると、EF Core を使って SQL Server からデータを取得する際当該列のデータに NULL が含まれていると、

System.Data.SqlTypes.SqlNullValueException: Data is Null. This method or property cannot be called on Null values.

・・・という例外がスローされます。下の画像を見てください。

SqlNullValueException

Microsoft のドキュメントによると、SqlNullValueException は「System.Data.SqlTypes 構造体の Value プロパティが null に設定されている場合にスローされる例外」ということだそうです。

メカニズムは不明ですが、string? 型のプロパティに RequiredAttribute 属性を付与し EF Core で SQL Server からデータを取得してくるときに、データに NULL があると「SqlTypes 構造体の Value プロパティが null に設定」ということになるようです。

SqlNullValueException の説明と RequiredAttribute 属性を設定したことが結びつかなくて、解決に悩んで無駄な時間を費やすことになるかもしれませんので注意してください。(実は自分がそうでした)

Tags: , , ,

ADO.NET

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月にこのブログを立ち上げました。その後ブログ2を追加し、ここはプログラミング関係、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  August 2022  >>
MoTuWeThFrSaSu
25262728293031
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar