WebSurfer's Home

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

画像をアップロードして SQL Server に保存 (CORE)

by WebSurfer 2021年11月24日 14:51

画像ファイルをタイトルと説明の文字列と共にアップロードし、サーバ側でサムネイル画像を作成してタイトル・説明・サムネイル画像・オリジナル画像を一式 SQL Server データベースに保存するサンプルを書きます。

一覧の表示

上の画像はアップロードして SQL Server に保存されたタイトル、説明、サムネイル画像を取得して一覧にして表示したものです。(オリジナル画像を表示してないのは一覧表に表示するのは大きすぎるからという理由だけです)

以下に、Entity Framework Code First の機能を使っての SQL Server データベースの作り方、一覧の表示、アップロード、編集、削除機能を実装した ASP.NET Core MVC アプリの作り方を述べます。

(保存先をデータベースではなく Web サーバーの特定のフォルダにファイルとして保存する場合は別の記事「ASP.NET Core MVC でファイルアップロード」に書きましたので、興味がありましたらそちらを見てください)

ベースに使ったプロジェクトは、先の記事「Visual Studio 2022 の ASP.NET MVC アプリ」に書いた Visual Studio 2022 で作成した .NET 6.0 の ASP.NET Core MVC アプリです。それに以下のように機能を追加します。

(1) Model

アップロードの際に Controller と View の間でデータのやり取りをするための View Model と、Entity Framework を使って SQL Server データベースとのデータのやり取りをするエンティティクラスを作ります。この記事では以下のようにしました (ファイル名とか MIME タイプ情報なども保持したいかもしれませんが、それはまた別の機会に)。

#nullable disable

using System.ComponentModel.DataAnnotations;

namespace MvcCore6App.Models
{
    // View Model
    // アップロードの際 Controller/View 間でデータのやり取りをする
    public class FileUploadViewModel
    {
        [Display(Name = "タイトル")]
        [Required(ErrorMessage = "{0} は必須")]
        [StringLength(25, ErrorMessage = "{0} は {1} 文字以内")]
        public string Title { get; set; }

        [Display(Name = "説明")]
        [StringLength(250, ErrorMessage = "{0} は {1} 文字以内")]
        public string Description { get; set; }

        [Display(Name = "ファイル")]
        [Required(ErrorMessage = "{0} は必須")]
        public IFormFile PostedFile { get; set; }
    }

    // エンティティクラス
    // SQL Server データベースとのデータのやり取りをする。また、
    // これをベースに EF Code First でデータベースを生成する
    public class FileEntity
    {
        public int Id { get; set; }

        [Display(Name = "タイトル")]
        [Required]
        [StringLength(25)]
        public string FileName { get; set; }

        [Display(Name = "説明")]
        [StringLength(250)]
        public string Description { get; set; }

        [Display(Name = "サムネイル画像")]
        [Required]
        public byte[] ThumbImage { get; set; }

        [Required]
        public byte[] OriginalImage { get; set; }
    }
}

Visual Studio 2022 で作成した .NET 6.0 の ASP.NET Core MVC アプリでは NULL 許容参照型がプロジェクト全体で有効化されていますので、警告を抑制するため #nullable disable を付与しています。下のコードでも必要に応じてそのようにしています。

(2) コンテキストクラス

Entity Framework を使って Controller と SQL Server データベースとの間でデータをやり取りするためのコンテキストクラスを定義します。この記事では以下のようにしました。

#nullable disable

using MvcCore6App.Models;
using Microsoft.EntityFrameworkCore;

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

        public DbSet<FileEntity> Files { get; set; }

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

(3) Program.cs にサービスの追加

上記 (2) で定義したコンテクストクラスのインスタンスを Controller のコンストラクタ経由で DI できるようにするため、Program.cs に AddDbContext メソッドを使ってサービスの追加を行います。以下のコードで「// これを追加」とコメントした部分です。

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using MvcCore6App.Data;
using MvcCore6App.Areas.Identity.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration
    .GetConnectionString("ApplicationDbContextConnection");

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));

builder.Services.AddDefaultIdentity<ApplicationUser>(options => 
    options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

// これを追加
builder.Services.AddDbContext<FileContext>(options =>
    options.UseSqlServer(connectionString));

// Add services to the container.
builder.Services.AddControllersWithViews();

// ・・・後略・・・

(4) SQL Server データベース作成

上に定義したエンティティクラスとコンテクストクラスをベースに Migration 操作を行って SQL Server データベースを生成します。

SQL Server データベース

具体的には、Visual Studio のパッケージマネージャーコンソールで Add-Migration, Update-Database コマンドを実行すれば、Entity Framework Code First の機能によって、上の画像のようなデータベースが生成されます。

なお、上の (3) で接続文字列を appsettings.json に既存の ASP.NET Identity 用の "ApplicationDbContextConnection" にしていますので、ASP.NET Identity のユーザー情報のストア用のデータベースに File テーブルが追加されます。別のデータベースとして作成したい場合は接続文字列を変更してください。

(5) サムネイル作成用ユーティリティ

オリジナル画像を指定したサイズに縮小したサムネイル画像を作成するユーティリティクラスを定義します。

今回は以前に作成した .NET Framework の Windows アプリ用のコードを流用したのですが、それは Windows OS の GDI+ に依存する System.Drawing 名前空間のグラフィックス機能を利用しています。

.NET Core では特定の OS に依存する機能は Visual Studio のテンプレートで作るプロジェクトには含まれてないようで、利用するには NuGet パッケージ System.Drawing.Common をインストールする必要があります。

System.Drawing.Common

Windows OS の GDI+ に依存するということで Linux 上で動かすと例外が出て動かないとのことですが、それを解決するために libgdiplus というライブラリがあるそうです。(未検証・未確認です)

NuGet パッケージ System.Drawing.Common をインストールしても CA1416 警告が出ますが、#pragma warning disable CA1416 を追記して警告を抑制しました。コードは以下の通りです。

#pragma warning disable CA1416

using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

namespace MvcCore6App.Utils
{
    public class ImageUtils
    {
        const int sizeThumb = 69;   // thumbimage のサイズ(縦横同じ)
        const int sizeLarge = 400;  // largerimage のサイズ(横幅)

        // sizeThumb で指定されたサイズのサムネイルを作る。
        // オリジナルの縦横比は保たれる(高さ or 幅の大きい方が sizeThumb になる)
        public static byte[] MakeThumb(byte[] fullsize)
        {
            // ・・・省略・・・
        }

        // 引数 newWidth, newHeight で指定されたサイズのサムネイルを作る。
        // 縦���で縮小率が異なる場合変形されないよう大きい方をトリミングして縮小
        public static byte[] MakeThumb(byte[] fullsize, int newWidth, int newHeight)
        {
            using (MemoryStream ms1 = new MemoryStream(fullsize))
            using (Image iOriginal = Image.FromStream(ms1))
            {                                
                // オリジナル/サムネイルの縦横のサイズ比
                double scaleW = (double)iOriginal.Width / (double)newWidth;
                double scaleH = (double)iOriginal.Height / (double)newHeight;

                // オリジナル画像をトリミングするための Rectangle 作成
                Rectangle srcRect = new Rectangle();

                if (scaleH == scaleW)  // 縦横同じ⇒トリミングなし
                {
                    srcRect.Width = iOriginal.Width;
                    srcRect.Height = iOriginal.Height;
                    srcRect.X = 0;
                    srcRect.Y = 0;
                }
                else if (scaleH > scaleW) // 縦 > 横 ⇒ 縦のみトリミング
                {
                    srcRect.Width = iOriginal.Width;
                    srcRect.Height = Convert.ToInt32((double)newHeight * scaleW);
                    srcRect.X = 0;
                    srcRect.Y = (iOriginal.Height - srcRect.Height) / 2;
                }
                else   // 縦 < 横 ⇒ 横のみトリミング
                {
                    srcRect.Width = Convert.ToInt32((double)newWidth * scaleH);
                    srcRect.Height = iOriginal.Height;
                    srcRect.X = (iOriginal.Width - srcRect.Width) / 2;
                    srcRect.Y = 0;
                }

                using (Image iThumb = new Bitmap(newWidth, newHeight))
                using (Graphics g = Graphics.FromImage(iThumb))
                {
                    g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                    Rectangle destRect = new Rectangle(0, 0, newWidth, newHeight);
                    g.DrawImage(iOriginal, destRect, srcRect, GraphicsUnit.Pixel);

                    using (MemoryStream ms2 = new MemoryStream())
                    {
                        iThumb.Save(ms2, ImageFormat.Jpeg);
                        return ms2.GetBuffer();
                    }
                }
            }
        }

        // 幅のみ指定してサムネイルを作る。高さは幅と同じ縮小率で縮小。
        public static byte[] MakeThumb(byte[] fullsize, int maxWidth)
        {
            // ・・・省略・・・
        }
    }
}

引数 fullsize のバイト列が有効なイメージ形式でないと Image.FromStream で例外がスローされます。一応 png と jpeg 形式は問題ないのは確認しましたが、その他はチェックはしてないので注意してください。戻り値のサムネイル画像のバイト列は jpeg 形式になります。

(6) Controller / Action Method

SQL Server のレコード一覧の表示、アップロード、編集、削除を行うための Controller のコードは以下のようにしました。

using Microsoft.AspNetCore.Mvc;
using MvcCore6App.Data;
using MvcCore6App.Models;
using MvcCore6App.Utils;
using Microsoft.EntityFrameworkCore;

namespace MvcCore6App.Controllers
{
    public class FileController : Controller
    {
        private readonly FileContext _context;

        public FileController(FileContext context)
        {
            _context = context;
        }

        public async Task<IActionResult> Index()
        {
            var fileContext = _context.Files;
            return View(await fileContext.ToListAsync());
        }

        public async Task<IActionResult> GetThumb(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var file = await _context.Files
                .FirstOrDefaultAsync(m => m.Id == id);

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

            return File(file.ThumbImage, "image/jpeg");
        }


        public IActionResult Upload()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Upload(FileUploadViewModel model)
        {
            if (ModelState.IsValid)
            {
                using (var memoryStream = new MemoryStream())
                {
                    await model.PostedFile.CopyToAsync(memoryStream);

                    // Upload the file if less than 2 MB
                    if (memoryStream.Length < 2097152)
                    {
                        var byteArray = memoryStream.ToArray();
                        var file = new FileEntity()
                        {
                            FileName = model.Title,
                            Description = model.Description,
                            ThumbImage = ImageUtils.MakeThumb(byteArray, 70, 70),
                            OriginalImage = byteArray
                        };

                        _context.Files.Add(file);

                        await _context.SaveChangesAsync();
                    }
                    else
                    {
                        ModelState.AddModelError("PostedFile", "サイズは 2MB 以下");
                        return View(model);
                    }
                }

                return RedirectToAction("Index");
            }

            return View(model);
        }

        public async Task<IActionResult> Edit(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var file = await _context.Files.FindAsync(id);
            if (file == null)
            {
                return NotFound();
            }
            return View(file);
        }

        [HttpPost, ActionName("Edit")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> EditPost(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var fileToUpdate = await _context
                                     .Files
                                     .FirstOrDefaultAsync(f => f.Id == id);

            if (fileToUpdate != null)
            {

                if (await TryUpdateModelAsync<FileEntity>(
                    fileToUpdate,
                    "",
                    f => f.FileName, f => f.Description))
                {
                    await _context.SaveChangesAsync();
                    return RedirectToAction("Index");
                }

            }
            return View(fileToUpdate);
        }

        public async Task<IActionResult> Delete(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var file = await _context.Files
                .AsNoTracking()
                .FirstOrDefaultAsync(f => f.Id == id);

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

            return View(file);
        }

        // POST: Students/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            var file = await _context.Files.FindAsync(id);

            if (file == null)
            {
                return RedirectToAction("Index");
            }

            _context.Files.Remove(file);
            await _context.SaveChangesAsync();
            return RedirectToAction("Index");
            
            
        }
    }
}

Upload メソッドは Microsoft のドキュメント「バッファー モデル バインドを使用して小さいファイルをデータベースにアップロードする」を参考にしました。Action Method と View の間のデータのやり取りにはビューモデル FileUploadViewModel を使用し、それをエンティティクラス FileEntity に移し替えるとともにサムネイル画像を追加し SQL Server データベースに保存しています。

アップロードするファイルの検証については、アップロードする時点でのファイル選択の有無とサーバー側でのサイズのチェックしか行っていません。Microsoft のドキュメント「ASP.NET Core でファイルをアップロードする」の「セキュリティに関する考慮事項」のセクションに書かれたセキュリティ関する配慮はしていませんので注意してください。

先の記事「ファイルアップロード時の検証 (CORE)」に、ASP.NET Core. 3.1 MVC アプリでファイルをアップロードする際に、カスタム検証属性を利用してファイルのサイズとタイプをクライアント側とサーバー側の両方で検証し、検証結果 NG の場合はエラーメッセージを表示する方法を書きましたので、興味があれば見てください。

Edit / EditPost メソッドではタイトルと説明のみ編集して結果を SQL Server データベースに反映するようにしました。画像の差し替えは上のコードではできません (画像を差し替えるなら、削除してから新たに Upload し直した方が良いと思いましたので)。

タイトルと説明のみ変更するため TryUpdateModelAsync メソッドを使っているところに注目してください。よくあるパターンとしては、上の EditPost メソッドの引数に ビューモデル FileUploadViewModel を使ってそれにモデルバインドということをすると思いますが、画像データはアップロードされてこないところが問題です。

上の EditPost メソッドのコードでは、既存のエンティティを読み取り、TryUpdateModel を呼び出してポストされたタイトルと説明からフィールドを更新しています。既存のエンティティの読み取りによって画像データも取得されるので、その上でタイトルと説明だけを変更して SaveChanges を適用するので画像データが消えることはないです。

TryUpdateModelAsync メソッドについては Microsoft のチュートリアルの「HttpPost Edit メソッドの更新」が参考になると思いますので興味があれば見てください。

GetThumb メソッドは、DB からサムネイル画像を取得してダウンロードするためのメソッドです。この記事の一番上の一覧表示の画像ようにサムネイルを表示するために使います。View に img 要素を配置し、その src 属性に GetThumb メソッドを設定することによりサムネイル画像が表示されます。

src 属性に GetThumb メソッドを設定するということは、ブラウザはそこで GetThumb メソッドを呼んでデータベースからデータを取得してくるという動きになることに注意してください。特にこの記事の一番上の画像のように一覧表示する場合はレコードの数だけ GetThumb メソッドが呼ばれることになります。

それを避けるのは、アクションメソッドですべてのデータを Model として View に渡し済みですので、View で Model に含まれる画像のバイト列を Data URL 形式に変換して src 属性に設定することにより可能です。その例は下の「(7) View」のセクションの Index.cshtml に書きます。ただし、そのようにした場合はブラウザはその画像をキャッシュできないことに注意してください。

(7) View

Index.cshtml, Upload.cshtml, Edit.cshtml, Delete.cshtml のコードをその順に以下に記載しておきます。Index.cshtml と Delete.cshtml には上のコントローラーのコードの GetThumb メソッドを使ってサムネイル画像を表示するようにしています。

Index.cshtml

@model IEnumerable<MvcCore6App.Models.FileEntity>

@{
    ViewData["Title"] = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<p>
    <a asp-action="Upload">Uoload New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.FileName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Description)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ThumbImage)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@{
#nullable disable
    foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.FileName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Description)
            </td>
            <td>
                @*以下はコメントアウトして代わりのコードを書いた                
                <img src="/File/GetThumb/@item.Id" 
                    alt="@item.FileName" title="@item.FileName" />*@

                @*アクションメソッドですべてのデータを Model として受け取って
                いるので、Model に含まれる画像のバイト列を Data URL 形式に変
                換して src 属性に設定することにより画像を表示できる。*@
                <img src="data:image/jpeg;base64,@Convert.ToBase64String(item.ThumbImage)" 
                    alt="@item.FileName" title="@item.FileName" />
            </td>
            
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
    }
}
    </tbody>
</table>

Upload.cshtml

@model MvcCore6App.Models.FileUploadViewModel

@{
    ViewData["Title"] = "Upload";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Upload</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Upload" enctype="multipart/form-data" method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Description" class="control-label"></label>
                <input asp-for="Description" class="form-control" />
                <span asp-validation-for="Description" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="PostedFile" class="control-label"></label>
                <input asp-for="PostedFile" type="file" class="form-control" />
                <span asp-validation-for="PostedFile" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Edit.cshtml

@model MvcCore6App.Models.FileEntity

@{
    ViewData["Title"] = "Edit";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Edit</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="FileName" class="control-label"></label>
                <input asp-for="FileName" class="form-control" />
                <span asp-validation-for="FileName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Description" class="control-label"></label>
                <input asp-for="Description" class="form-control" />
                <span asp-validation-for="Description" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Delete.cshtml

@model MvcCore6App.Models.FileEntity

@{
    ViewData["Title"] = "Delete";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Delete</h1>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>FileEntity</h4>
    <hr />
    <dl class="row">
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.FileName)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.FileName)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Description)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Description)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.ThumbImage)
        </dt>
        <dd class = "col-sm-10">
            @{
                if (Model != null)
                {
                    <img src="/File/GetThumb/@Model.Id" 
                        alt="@Model.FileName" 
                        title="@Model.FileName" />
                }
            }
        </dd>
    </dl>
    
    <form asp-action="Delete">
        <input type="hidden" asp-for="Id" />
        <input type="submit" value="Delete"
            class="btn btn-danger" /> |
        <a asp-action="Index">Back to List</a>
    </form>
</div>

Tags: , , , ,

CORE

EXECUTE を使うストアドプロシージャ

by WebSurfer 2021年10月8日 14:00

必要な最小の権限のみ与えるというセキュリティの基本に沿って、データを操作するのに必要なストアドプロシージャを作って、ユーザーにはストアドプロシージャに対する実行権限だけを与えるのが良いという話を聞きます (データベースやテーブル全体に対する権限を与えるのではなくて)。

しかしながら、EXECUTE ステートメントを使用するストアドプロシージャの場合、ストアドプロシージャに対する実行権限だけでは権限が不足するケースがあるということを書きます。

エラーメッセージ

上の画像はストアドプロシージャを使ってデータベースからデータを取得して表示する ASP.NET Web Forms アプリの例で、権限不足のため SqlException 例外がスローされ、「SELECT 権限がオブジェクト 'Student'、データベース 'TestDatabase'、スキーマ 'dbo' で拒否されました」というエラーメッセージが表示されています。

IIS 上で動く ASP.NET Web Forms アプリなので、そのワーカープロセスのアカウント NETWORK SERVICE にストアドプロシージャに対する実行権限は与えてあります。

そのストアドプロシージャは以下のとおりで、SELECT クエリを含む文字列を EXECUTE ステートメントで実行するようになっています。

ストアドプロシージャその 1

エラーメッセージは SELECT 権限が拒否されましたと言っています。つまり、今回の例では、NETWORK SERVICE に対象テーブル対する SELECT 権限を与える必要があると言っています。

なので、エラーメッセージに従って SELECT 権限を与えれば動くはずです。実際にやってみましたが、ストアドプロシージャの実行権限に加えて、対象テーブルに対する SELECT 権限を与えれば動きました。下の画像がアプリを実行した結果です。

アプリの実行結果

何故ストアドプロシージャの実行権限だけではダメなのかを調べてみると、そういう仕様のようです。Microsoft のドキュメント EXECUTE (Transact-SQL) の「アクセス許可」のセクションに以下の通り書いてありました。

"EXECUTE ステートメントの実行に権限は必要ありませんが、 EXECUTE 文字列内で参照されるセキュリティ保護可能なリソースに対しては権限が必要です。 たとえば、この文字列に INSERT ステートメントが含まれている場合、EXECUTE ステートメントの呼び出し元は対象のテーブルに対する INSERT 権限が必要です。"

(上の「EXECUTE ステートメントの実行に権限は必要ありませんが」というのはストアドプロシージャの「実行」権限の話ではありません。ユーザーにはストアドプロシージャの「実行」権限は必ず与える必要があります)

では、EXECUTE ステートメントを使わないストアドプロシージャ即ち以下のような場合はどうなるでしょうか? 実際に検証した結果、こちらはストアドプロシージャに対する実行権限だけを与えればよく、対象テーブル対する SELECT 権限は不要でした。(この違いが分かり難く間違いのもとになりそうです)

ストアドプロシージャその 2

もう一つ、EXECUTE + sp_executesql (Transact-SQL)を使ったらどうなるか、即ち以下のようなストアドプロシージャではどうかも試してみました。

ストアドプロシージャその 3

結果はやはり、ストアドプロシージャの実行権限に加えて、対象テーブルに対する SELECT 権限も必要でした。


以上でメインの話は終わりですが、検証に使ったテーブル、権限の与え方、ASP.NET Web Forms アプリのコードを忘れないように以下にメモしておきます。

検証に使った TestDatabase データベース内の Student テーブルは以下の通りです。

Student テーブル

NETWORK SERVICE は SQL Server のログインに設定済みです。サーバーロールはデフォルトの public だけです(public は必ず付与され、外すことはできません)。ユーザーマッピングで TestDatabase のマップにチェックを入れます。

ユーザーマッピング

public サーバーロールには接続権限が許可されていますので、上の操作で自動的に NETWORK SERVICE に TestDatabase データベースに対する接続権限が与えられます。

データベースに対する権限の設定

ストアドプロシージャに対する実行権限の設定。これだけでは権限不足でこの記事の一番上の画像のエラーとなります。

ストアドプロシージャに対する実行権限の設定

Student テーブルに対する SELECT 権限の設定を行います。

Student テーブルに対する SELECT 権限の設定

検証に使った ASP.NET Web Forms アプリのコードは以下の通りです。ストアドプロシージャ経由 Student テーブルからデータを取得して List<T> 型のオブジェクトを生成し、それを GridView にバインドしてレコード一覧を表示しています。

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;

public partial class test03 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string connStr = @"接続文字列";
        using (var connection = new SqlConnection(connStr))
        {
            using (var command = new SqlCommand())
            {
                command.Connection = connection;
                command.CommandType = CommandType.StoredProcedure;
                command.CommandText = "[dbo].[StoredProcedure1]";
                var list = new List<StudentDTO>();

                connection.Open();                
                using (var reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        var student = new StudentDTO();
                        student.StudentID = reader.GetInt32(0);
                        student.FirstName = reader.IsDBNull(1) ? 
                            null : reader.GetString(1);
                        student.LastName = reader.IsDBNull(2) ? 
                            null : reader.GetString(2);
                        student.Birthday = reader.IsDBNull(3) ? 
                            null : (DateTime?)reader.GetDateTime(3);
                        student.Gender = reader.IsDBNull(4) ? 
                            null : reader.GetString(4);
                        list.Add(student);
                    }
                }

                GridView1.DataSource = list;
                GridView1.DataBind();
            }
        }
    }
}

public class StudentDTO
{
    public int StudentID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime? Birthday { get; set; }
    public string Gender { get; set; }
}

Tags: , , ,

SQL Server

SQL Server の数値型を LIKE 句を使ってあいまい検索

by WebSurfer 2021年6月19日 12:55

SQL Server の数値型は、文字列型と同様に、そのまま直接 LIKE 句を使ってあいまい検索ができるようです。下の画像を見てください。UnitPrice 列は money 型ですが、[UnitPrice] LIKE '%2%' という条件が有効になっています。

SSMS での検索結果

知ってました? 実は何を隠そう自分は最近まで知らなかったです。(汗) 数字型はまず文字列型に変換してから、それに LIKE 句を使うものだと思ってました。

調べてみると、Microsoft のドキュメント「LIKE (Transact-SQL)」に、

"引数が文字列データ型でない場合、SQL Server データベース エンジン は可能であれば引数を文字列データ型に変換します。 If any one of the arguments isn't of character string data type, the SQL Server Database Engine converts it to character string data type, if it's possible."

・・・と書いてあります。実際に試してみると、上の画像のように money 型の UnitPrice 列も LIKE 句を使ってあいまい検索ができました。

Microsoft のドキュメントが言う「可能であれば」がどこまでの範囲か調べ切れていませんが、自分が SQL Server 2012 で試した限りでは int 型と money 型は可能な範囲に入るようです。

ADO.NET + SqlClient を使った .NET Framework のアプリケーションでも同じことができます。パラメータ化も可能です。ただし、パラメータ化する場合は、パラメータは文字列型として扱う必要がありますが。

上の画像と同様な LIKE 句を使って検索を行う .NET Framework コンソールアプリのサンプルコードを以下に載せておきます。ADO.NET + SqlClient を使い、SQL 文はパラメータ化しています。

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;

namespace ConsoleAppLIKE
{
    public class Product
    {
        public int ProductID { get; set; }

        public string ProductName { set; get; }

        // UnitPrice 列は NULL 可なので Nullable とした
        public decimal? UnitPrice { set; get; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            string connString = "接続文字列";
            string selectQuery =
                "SELECT [ProductID] ,[ProductName] ,[UnitPrice] FROM [Products] " +
                "WHERE [ProductName] LIKE N'%' + @ProductName + N'%' AND " +
                "[UnitPrice] LIKE N'%' + @UnitPrice + N'%'";

            string productName = "ch";
            string unitPrice = "2";            
            List<Product> productList = new List<Product>();

            using (var connection = new SqlConnection(connString))
            {
                using (var command = new SqlCommand(selectQuery, connection))
                {
                    var p1 = new SqlParameter("@ProductName", SqlDbType.NVarChar);
                    p1.Value = productName;

                    // UnitPrice 列は money 型だが LIKE 句を使ってあいまい検索
                    // する場合はパラメータの型は文字列とする
                    var p2 = new SqlParameter("@UnitPrice", SqlDbType.NVarChar);
                    p2.Value = unitPrice;

                    command.Parameters.Add(p1);
                    command.Parameters.Add(p2);

                    connection.Open();
                    using (var reader = command.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            var product = new Product
                            {
                                ProductID = reader.GetInt32(0),
                                ProductName = reader.GetString(1),

                                // UnitPrice 列は NULL 可なのでその対応
                                UnitPrice = reader.IsDBNull(2) ?
                                            null : (decimal?)reader.GetDecimal(2)
                            };

                            productList.Add(product);
                        }
                    }
                }
            }

            foreach (Product p in productList)
            {
                Console.WriteLine($"PriductID: {p.ProductID}, " +
                    $"ProductName: {p.ProductName}, UnitPrice: {p.UnitPrice}");
            }
        }
    }
}

サンプルコード中のコメントにも書きましたが、UnitPrice 列は money 型ですが LIKE 句を使ってあいまい検索する場合はパラメータの型は文字列とする必要がありますので注意してください (例えば、SqlDbType.NVarChar を SqlDbType.Deciaml にするとエラーになります)。

上のコードの実行結果は以下の通りで、上の画像の SSMS での実行結果と同じになっています。

ADO.NET での検索結果

Tags: , , ,

SQL Server

About this blog

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

Calendar

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

View posts in large calendar