WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

SQL Server オブジェクトエクスプローラー

by WebSurfer 2022年8月13日 14:09

自分の環境の Visual Studio 2022 では下の画像のように SQL Server オブジェクトエクスプローラーが表示されていますが、その中の各項目が何かを調べたので備忘録として書いておきます。

SQL Server オブジェクトエクスプローラー

まず、SQL Server オブジェクト エクスプローラーとは何かですが、CodeZin の記事 使わなきゃ損! SQL Serverの新たな開発ツール「SQL Server Data Tools」によると SQL Server Data Tools (SSDT) の機能で、"開発者がSQL Server Management Studioで実施していたテーブルの作成や変更などのデータベース関連タスクを、Visual Studioで完結することを目的としています" というものだそうです。

Visual Studio 2022 に SSDT をインストールするには、Microsoft のドキュメント SQL Server Data Tools (SSDT) for Visual Studio のダウンロードに書いてあるように、ワークロードの「データの保存と処理」を追加し、そのオプションの「SQL Server Data Tools」にチェックを入れます。

そして、Visual Studio 2022 のメニューバーから[表示(V)]⇒[SQL Server オブジェクト エクスプローラー(S)]をクリックすれば SQL Server オブジェクトエクスプローラーが表示されます。

SQL Server オブジェクトエクスプローラーを表示

前置きが長くなりましたが、上の SQL Server オブジェクトエクスプローラーの画像に表示されている各項目が何かを説明します。

(1) (local)\sqlexpress (SQL Server 11.0.2100 ...)

ローカルにインストールした SQL Server 2012 Express の名前付きインスタンスです。

SQL Server の Express 版をインストールすると、デフォルトでは「名前つきインスタンス」となり、インスタンス名は SQLEXPRESS になります。

(記憶にないですが、たぶん、自分で SQL Server オブジェクト エクスプローラーを操作して追加したものだと思います)

(2) (localdb)\MSSQLLocalDB (SQL Server 13.0.4001 ...)

SQL Server 2016 LocalDB の自動インスタンスです。13.0.4001 から SQL Server 2016 ベースであることが分かります。

他に名前付きインスタンスというのもあります。詳しくは Microsoft のドキュメント SQL Server Express LocalDB を見てください。

自動インスタンスとは SQL Server の既定のインスタンスに該当するもののようで、開発時にはそれに接続して使うようにします。

Microsoft のドキュメントに書いてある通り、自動インスタンスの名前は MSSQLLocalDB になります。(SQL Server 2014 で変更されたそうです。その前は、文字 v の後に LocalDB とバージョン番号を付けたものでした)

Visual Studio 2019 では SQL Server 2016 LocalDB が、Visual Studio 2022 では SQL Server 2019 LocalDB が一緒にインストールされます。

自分の PC には Visual Studio 2019 / 2022 両方をインストールしており、一緒にインストールされた LocalDB は以下のようになっています。(一番上の SQL Server 2014 Express LocalDB は Visual Studio 2015 と一緒にインストールされたもの) 

インストールされている LocalDB

それなのに、なぜ Visual Studio 2022 の SQL Server オブジェクトエクスプローラーに表示されている自動インスタンスが SQL Server 2019 LocalDB のものではないのでしょう? それはたぶん以下のような話ではないかと思います。

上に紹介した Microsoft のドキュメントに以下のように書いてあります。

"ユーザーのコンピューターにインストールされているどのバージョンの LocalDB についても、LocalDB の自動インスタンスが 1 つ存在します"(PC 内に複数の自動インスタンスは存在しないということ)

"あるコンピューター上でユーザーが初めて LocalDB への接続を試みるときは、自動インスタンスを作成し、なおかつ開始する必要があります"

ということで、先に Visual Studio 2019 で作業したとき SQL Server 2016 LocalDB で自動インスタンスが作成され、Visual Studio 2022 でも先に作成された自動インスタンスがそのまま使われているということだと思います。

その自動インスタンスを、コマンド ライン管理ツール: SqlLocalDB.exe を使って SQL Server 2019 LocalDB にアップグレードすることはできるようです。

ググって調べると、Upgrade Visual Studio 2019’s LocalDB to SQL 2019 とか Upgrading SQL Server LocalDb などの方法を書いた記事がヒットします。

その方法というのは、(a) 既存の自動インスタンスを削除、(b) 新たに SQL Server 2019 LocalDB の自動インスタンスを作成、(c) 既存のデーターベースを新たに作成した自動インスタンスにアタッチする・・・ということになるようです。

上の (c) にリスクがありそうです。SQL Server 2019 LocalDB の自動インスタンスが必須というわけではない現状では、アップグレードには手を出さない方が良さそうな感じです。

(3) (localdb)\ProjectModels (SQL Server 15.0.4153 ...)

これは自分で作った記憶がなくて、Visual Studio 2022 で SSDT 関係の操作をしたとき自動的に作られたもののようです。

Microsoft の Developer Community の記事 (localdb)\ProjectsV13 not setup when installing Visual Studio 2022 に説明がありました。

自分の環境では、Visual Studio 2019 で、一緒にインストールされた SQL Server 2016 LocalDB をベースに、(localdb)\ProjectsV13 という名前のインスタンスが作られて、それがそのまま残っている。その後 Visual Studio 2022 を使うようになって、一緒にインストールされた SQL Server 2019 LocalDB をベースに (localdb)\ProjectModels という名前のインスタンスが作られたということのようです。

Visual Studio にインストールした SSDT が使うもので、開発者がアプリで使うものではないようです。

(4) (localdb)\ProjectsV13 (SQL Server 13.0.4001 ...)

上にも書きましたが、Visual Studio 2019 で一緒にインストールされた SQL Server 2016 LocalDB をベースに (localdb)\ProjectsV13 という名前のインスタンスが作られたようです。

一体それは何かですが、Stsckoverflow の記事 Purpose of ProjectsV13 LocalDB instance に以下のように書いてありました。

"The primary reason is to avoid conflicts with any "production" databases on MSSQLLocalDB. SSDT creates a new database for every database project you open. If your project is called Adventureworks, this might conflict with an Adventureworks database created by web projects or that are used by local ASP.NET applications you are running / debugging. Since SSDT does this automatically, in the background, it was felt that there was too high a risk of conflict. Hence, a separate instance is used."

上の (3) の (localdb)\ProjectModels も同じだと思います。

Tags: , , ,

DevelopmentTools

null 許容参照型と EF Core Code First

by WebSurfer 2022年5月12日 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! を追加するなどしていますが基本は同じ)

(注: EF Core 7.0 以降では、DbContext と DbSet に "EF がリフレクションを使用してこれらのプロパティを自動的に初期化するため、この警告は抑制されます" と書いてある通り、下の Blogs, Posts プロパティに = 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 許容か否かも、データベースの当該フィールドの NULL 可/不可と同じになります。

ナビゲーションプロパティの型については注意が必要です。例えば、上の画像の dbo.Blogs, dbo.Posts テーブルからリバースエンジニアリングでエンティティクラスを作成した場合、dbo.Blog テーブルの外部キーフィールド BlogId が NULL 不可になっているため、Blog クラスの Posts ナビゲーションプロパティと、Post クラスの Blog ナビゲーションプロパティの型は Null 許容にはなりません。

そうなるとどういう問題が起きるかと言うと、エンティティクラスをビューモデルに使ってブラウザからのデータを MVC アプリのアクションメソッドで受け取る場合、モデルバインディングでナビゲーションプロパティには null が代入されるので、ModelState.IsValid が false になり、Create, Edit に失敗することです。解決策は、生成されたコードに手を加えて Null 許容にすることです。

試しに 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

Session 情報のストアに SQL Server を利用 (CORE)

by WebSurfer 2022年3月13日 15:52

ASP.NET Core アプリで Session 状態を利用する際に SQL Server をストアとして利用する方法を書きます。下の画像の赤線部分は Session 情報を Controller で ViewBag に設定しそれを表示したものです。

Session 状態の表示

ASP.NET Core アプリでの Session の使い方の詳しい話は Microsoft のドキュメント「ASP.NET Core でのセッションと状態の管理」にあります。

それを読めば大体分かると思いますが、.NET Framework 版の ASP.NET アプリの Session との大きな違いで自分が気が付いた点を以下にまとめておきます。

  1. Program.cs (.NET 6.0) または Startup.cs (.NET 5.0 以前) でサービスとミドルウェアの登録が必要。
  2. ASP.NET Core の分散キャッシュをデータのストアに利用する。 (なので、メモリ内キャッシュを使用する場合であってもセッションデータはシリアル化可能なでなければならない)
  3. String 型、Int32 型、Byte[] 型以外のデータ型は格納できない。(String 型、Int32 型、Byte[] 型以外のデータを格納する場合は JSON 文字列にシリアライズするなどの方法で格納する)
  4. 同時アクセスでもロックされず後書き優先になる。 (先の記事「SessionStateModule によるロック」で書いたロック機能は提供されてない)
  5. StateServer モードに相当する機能は「分散 Redis キャッシュ」を使って実現するらしい。(未検証・未確認です)

上の 2 に書いたように Session 情報のストアには分散キャッシュを利用するのですが、それには以下があるそうです。詳しくは Microsoft のドキュメント「ASP.NET Core の分散キャッシュ」を見てください。

  • 分散メモリ キャッシュ - AddDistributedMemoryCache
  • 分散 SQL Server キャッシュ - AddDistributedSqlServerCache
  • 分散 Redis キャッシュ - AddStackExchangeRedisCache
  • 分散 NCache キャッシュ - AddNCacheDistributedCache

上の「分散メモリキャッシュ」というのは、サーバーで実行されている Web アプリのメモリを使って ASP.NET Core の分散キャッシュを実現するためのもので、実際に「分散」されているわけではないそうです。

「分散メモリキャッシュ」を利用しての Session 状態の構成方法や使い方は Microsoft のドキュメント「ASP.NET Core でのセッションと状態の管理」に詳しく書いてありますのでそれを読めば容易に実装できると思います。

この記事では「分散 SQL Server キャッシュ」を利用しての Session 状態の構成方法を書きます。以下の説明と合わせて、Microsoft のドキュメントの「分散 SQL Server キャッシュ」のセクションも見てください。

(1) SQL Server のテーブルを作成

sql-cache create コマンドを実行して SQL Server にテーブルを作成します。以下の点に注意してください。

  • dotnet-sql-cache ツールをインストールしておく必要があります。dotnet tool install --global dotnet-sql-cache コマンドでインストールできます。
  • コマンドの接続文字列の Initial Catalog に設定するデータベースは事前に作成しておく必要があります。その際、sql-cache create コマンドを実行するユーザーのアカウントが SQL Server のログインに設定されていて、そのデータベースに対してテーブルを作成する権限を持っている必要があります。

この記事では開発マシンの SQL Server 2012 Express の既存のデータベース TestDatabase に Windows 認証で接続し、TestCache という名前でテーブルを作成しました。その結果は以下の画像の通りです。

SQL Server のテーブル

Microsoft ドキュメントの説明とは Id の Data Type が nvarchar(900) ではなくて nvarchar(499) と異なりますが、その他は同じ内容で TeestCache テーブルが生成されています。

(2) NuGet パッケージのインストール

NuGet パッケージ Microsoft.Extensions.Caching.SqlServer をインストールします。下の (3) 項でのサービスの登録で AddDistributedSqlServerCache を使うのに必要になります。

Microsoft.Extensions.Caching.SqlServer

(3) サービスとミドルウェアの登録

Program.cs (.NET 6.0) または Stratup.cs (.NET 5.0 以前) でサービスとミドルウェアを登録します。以下の例は Visual Studio 2022 で作成した .NET 6.0 の Program.cs での例です。

// ・・・前略・・・

// SQL Server 分散キャッシュを使用
// "DistCacheConnectionString" は appsettings.json に設定する
// SQL Server への接続文字列
builder.Services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString = 
        builder.Configuration.GetConnectionString(
                               "DistCacheConnectionString");
    options.SchemaName = "dbo";
    options.TableName = "TestCache";
});

// AddSession の呼び出し
builder.Services.AddSession(options =>
{
    // デフォルトは 20 分。検証用に 300 秒に設定
    options.IdleTimeout = TimeSpan.FromSeconds(300);

    options.Cookie.HttpOnly = true;  // デフォルトで true

    // デフォルトは false。true に設定しないとセッション状態は
    // 機能しない可能性あり
    options.Cookie.IsEssential = true;  
});

// ・・・中略・・・

// UseSession の呼び出し
// 順序が重要で、UseRouting の後かつ MapRazorPages と 
// MapControllerRoute の前に呼び出します
app.UseSession();

// ・・・後略・・・

(4) appsettings.json に接続文字列を設定

上の (3) 項の AddDistributedSqlServerCache メソッドで指定した "DistCacheConnectionString" という名前で構成ファイルから接続文字列を取得できるよう、appsettings.json ファイルに以下の設定を追加します。

"ConnectionStrings": {
  "DistCacheConnectionString": 
    "Data Source=(local)\\sqlexpress;Initial Catalog=TestDatabase;Integrated Security=True;"
}

上の接続文字列は SQL Server Express の sqlexpress という名前付きインスタンスにアタッチされている TestDatabase というデータベースに Windows 認証でローカルに接続するものです。そのあたりは個人の環境に合わせて変更してください。


以上で Session 状態は使えるようになります。

例えば HomeController で以下のように Session を設定して実行し、Home/Privacy に遷移した後で Home/Index に戻るとこの記事の一番上の画像の赤線で示したように Session データが表示されます。

public IActionResult Index()
{
    // Session は home/privacy で設定
    ViewBag.Name = HttpContext.Session.GetString("name");
    ViewBag.Test = HttpContext.Session.GetString("test");
    HttpContext.Session.Clear();

    return View();
}

public IActionResult Privacy()
{
    // Session の設定
    HttpContext.Session.SetString("name", "session value");
    HttpContext.Session.SetString("test", "test value");

   return View();
}

SQL Server に格納された Session データは以下のようになります。Chrome と Edge を立ち上げて両方からアクセスした結果で、それぞれ Session Id (デフォルトで .AspNetCore.Session という名前の cookie に格納されている) が異なるので、TestCache テーブルには Id が異なるレコードが 2 つ存在するという結果になっています。

SQL Server に格納された Session データ

上の画像のレコードはクライアントがブラウザを閉じてもそのまま残ります。と言っても、それが再利用されることはなく、再度ブラウザを立ち上げてアクセスして Session を使うと別の Id でレコードが追加されます。

上の Home/Index のコードにある HttpContext.Session.Clear() でも SQL Server のレコードは削除されません。Session Cookie も削除されません。Value の中のデータが削除され、ExpiresAtTime がその時点から AddSession メソッドの options.IdleTimeout で設定された時間まで延長されるのみです。

サーバー側で Session を使っているユーザーがオンラインか否かを判定する術はなく、サーバーではレコード削除の可否を判断できないのでそうせざるを得ないのではないかと思います。

ということはレコードは削除されずどんどん増えていく一方になるのではと思いましたが、翌日にまた調べてみたら、SQL Server には昨日のレコードは残っていたが、アプリを動かしてみると昨日のレコードは消えました。どうやら、アプリを起動する際(多分 Program.cs のコードが実行される時)に ExpiresAtTime が古いレコードは消去されるらしいです。

IIS を使ってのインプロセスホスティングモデルであればワーカープロセスがリサイクルされる時に Program.cs のコードが実行される(古いレコードは削除される)と思いますが、それ以外のホスティングモデルで Kestrel が使われる時はどうなるかは分かりません。そこは今後の検討課題ということにしたいと思います。

Tags: , ,

CORE

About this blog

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

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar