WebSurfer's Home

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

親子関係のあるデータ登録

by WebSurfer 2014年12月21日 15:59

先の記事 MVC4 EF Code First では、Entity Framework Code First の機能を利用して、MVC4 インターネットアプリケーションの SQL Server データベースに Parents と Children という親子関係を持つ 2 つのテーブルを作成しました。

そのアプリケーションに Create アクションメソッドとビューを追加して、Parents テーブルと Children テーブルに親と子のデータを同時に登録する方法について書きます。

Create

クライアントから上のようなユーザー入力画面を使って送信されてくるデータ(上の例では "日本太郎"、"日本花子"、"日本一郎" という 3 つの文字列)を、Web サーバーで受け取って、Create アクションメソッドのパラメータにバインド(モデルバインディング)してやる必要があります。

モデルバインディングとデータアノテーション検証(クライアントサイドを含む)が正しく行われるためには、以下の 2 つの点を考慮する必要があります。

  1. input 要素の name 属性はデータがコレクションの場合 "prefix[index].Property" というパターンにする。
  2. クライアント側での検証に必要な属性が追加されるよう、ビューに EditorFor のような Html ヘルパーを使う。

詳しくは、先の記事「コレクションのデータアノテーション検証」に書きましたので、興味がありましたら読んでください。

上の 2 点を考慮に入れて作った Controller の Create アクションメソッドと View のコードを下にアップしましたので見てください。

プラウザから /ParentChild/Create を GET 要求すると一番上の画像のように表示されます。そこでデータを入力して[Create]ボタンをクリックすると、クライアント側で入力データが検証されたあと、 [HttpPost] 属性を付与した方の Create アクションメソッドに POST されます。

下の画像(Visual Stidio でのデバッグ画面)を見てください。POST されてきたデータは Create アクションメソッドの parent パラメータに正しくモデルバインディングされています。

Create

その際、付与したアノテーション属性(この記事の例では Parent, Child クラスの Name プロパティに付与した Required と StringLength)によってサーバー側で入力データの検証が行われ、その結果が ModelStateDictionary(Controller.ModelState プロパティで取得できます)に格納されます。

検証結果が OK(ModelState.IsValid が true)であれば、送信されてきたデータは SQL Server の Parents, Children テーブルに登録されます。

Controller

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Mvc4App2.Models;

namespace Mvc4App2.Controllers
{
  public class ParentChildController : Controller
  {
    private ParentChildContext db = new ParentChildContext();
        
    //・・・中略・・・

    //
    // GET: /ParentChild/Create
    // 引数 number は登録する子のレコード数。
    // 今回はとりあえずデフォルトで 2 としてみた。
    public ActionResult Create(int number = 2)
    {
      Parent parent = new Parent();
      for (int i = 0; i < number; i++)
      {
        Child child = new Child();
        parent.Children.Add(child);
      }
      return View(parent);
    }

    //
    // POST: /ParentChild/Create
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(Parent parent)
    {
      if (ModelState.IsValid)
      {
        db.Parents.Add(parent);

        // 2016/9/6 追記:
        // 以下のコードは不用でした。db.Parents.Add(parent) だけ
        // で親子両方の EntityState が Added になり、SaveChanges
        // メソッドで DB に親子とも INSERT されます。
        //for (int i = 0; i < parent.Children.Count; i++)
        //{
        //  db.Children.Add(parent.Children[i]);
        //}

        db.SaveChanges();
        return RedirectToAction("Index");
      }
      return View(parent);
    }

    //・・・中略・・・
  }
}

今回は簡略化のため、とりあえず、親 : 子 = 1 : 2 で固定としました。 1 : n で n をユーザーが設定できるようにする方法も別途書く予定です。

SQL Server の Parents テーブルの Id 列と Children テーブルの Parent_Id 列には外部キー制約が設けられていますので、先に Parents に親レコードを INSERT してから Children に子レコードを INSERT する必要があります(つまり階層更新が必要)。また、Id 列は IDENTITY なので INSERT 操作が完了するまで値が分からないという問題もあります。そのあたりは、仕組みは不明ですが、Entity Framework がうまくやってくれるようで、上のコードで問題なく Create できます。

View

@model Mvc4App2.Models.Parent

@{
  ViewBag.Title = "Create";
  Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Create</h2>

@using (Html.BeginForm()) {
  @Html.AntiForgeryToken()
  @Html.ValidationSummary(true)

  <fieldset>
    <legend>Book</legend>

    <div class="editor-label">
      @Html.LabelFor(model => model.Name)
    </div>
    <div class="editor-field">
      @Html.EditorFor(model => model.Name)
      @Html.ValidationMessageFor(model => model.Name)
    </div>

    <hr />
    
    @for (int i = 0; i < Model.Children.Count; i++)
    {       
      <div class="editor-label">
        @Html.LabelFor(model => model.Children[i].Name)
      </div>
      <div class="editor-field">
        @Html.EditorFor(model => model.Children[i].Name)
        @Html.ValidationMessageFor(model => 
                            model.Children[i].Name)
      </div>

      <hr />
    }

    <p>
      <input type="submit" value="Create" />
    </p>
  </fieldset>
}

<div>
  @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
  @Scripts.Render("~/bundles/jqueryval")
}

EditorFor(model => model.Name) からは name="Name"、EditorFor(model => model.Children[i].Name) からは name="Children[i].Name"(i は連番)という name 属性が生成されます。

SQL Server の Parents, Children テーブルで、Id 列はいずれも IDENTITY となっています。ユーザー入力は不要なのでその入力用の EditorFor ヘルパーは設けていません。

コードの最後の方の @Scripts.Render("~/bundles/jqueryval") はクライアント側でのユーザー入力検証用の jQuery ライブラリを登録するためのものです。これがないとクライアント側での検証はかかりませんので注意してください。


以上、とりあえず Create するまでを書きました。後日、別途、Edit および Delete する方法も書く予定です。階層更新は Delete がちょっと面倒な気がします。

Tags:

MVC

MVC4 EF Code First

by WebSurfer 2014年12月20日 20:41

今さらながらですが、ASP.NET MVC ベースの Web アプリケーションを開発する際、Entity Framework の Code First の機能を使って SQL Server データベースを生成してみましたので、その話を備忘録として書いておきます。

生成した DB 内容の表示

基本的には @IT の記事「第2回 Entity Frameworkコード・ファーストでモデル開発」に書いてあった通りの方法で作成してみました。

ただし、環境は MVC3 ⇒ MVC4、EF4 ⇒ EF6、SQL Server Compact 4.0 ⇒ SQL Server 2008 Express と異なります。

また、作成するテーブルも少し簡略化しました。(もちろん @IT の記事の通りに SQL Server にテーブルを作成できます。簡略化したのは、後日、今回作成したデータベースを使って別の記事を書くためです)

Visual Studio 2010 への MVC4 のインストール方法、ベースとなる MVC4 のインターネットアプリケーションの説明については以下のページを見てください。

Entity Framework Code First の機能を使えば、結果として生成されるデータベースのスキーマ等は考える必要はなくなるというような話を聞きますが、個人的には、そんなことはないと思ってます。

単一のテーブルならそうかもしれませんが、今回説明する例のように、Code First の機能によって 2 つのテーブルを生成し、それに外部キー制約がついているような場合は 階層更新 を考えなければなりません。

まぁ、それは次の記事の話として書くとして、今回は Code First の機能を利用して、外部キー制約のある 2 つのテーブルを作るところまでを書きます。

Model

まず、テーブル生成の大元になる Model ですが、以下の例を考えます。親子関係にある Parent クラス と Child クラスおよび ParentChildContext クラスに注目してください。ParentChildInitializer クラスは初期化のためのもの(イニシャライザ)です。

ParentChildContext クラスに設定されている接続文字列名 "DefaultConnection" は、Visual Studio の「MVC4 のインターネットアプリケーション」テンプレートでアプリケーションを作成すると自動的に web.config に設定されるものです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Web.Mvc;
using System.Data.Entity;

namespace Mvc4App2.Models
{
  public class ParentChildInitializer : 
      DropCreateDatabaseIfModelChanges<ParentChildContext>
  {
    protected override void Seed(ParentChildContext context)
    {
      // とりあえずレコードを 2 つ作成してみた。
      // Id は IDENTITY になるので設定不用。
      var parents = new List<Parent> {
          new Parent { Name = "親の名1" },
          new Parent { Name = "親の名2" },
      };

      parents.ForEach(p => context.Parents.Add(p));
      context.SaveChanges();
    }
  }    
    
  public class ParentChildContext : DbContext
  {
    // 接続文字列名 DefaultConnection を指定。
    // 指定しないとクラス名 ParentChildContext
    // で探しにいくので注意。
    public ParentChildContext()
          : base("DefaultConnection")
    {
    }

    public DbSet<Parent> Parents { get; set; }
    public DbSet<Child> Children { get; set; }
  } 
    
  public class Parent
  {
    public Parent()
    {
      Children = new List<Child>();
    }

    public int Id { get; set; }

    [Required(ErrorMessage = "{0} は必須")]
    [StringLength(5, ErrorMessage = "{0} は {1} 文字以内")]
    [Display(Name = "Parent Name")]
    public string Name { get; set; }

    public virtual IList<Child> Children { get; set; }
  }

  public class Child
  {
    public int Id { get; set; }

    [Required(ErrorMessage = "{0} は必須")]
    [StringLength(5, ErrorMessage = "{0} は {1} 文字以内")]
    [Display(Name = "Child Name")]
    public string Name { get; set; }
  }
}

ParentChildContext クラスに定義されている Parents, Children プロパティが DB 上のテーブルに、Parent クラス と Child クラスがそれぞれのテーブル内のレコードに該当します。

Parent, Child クラスに定義されている Id という名前のプロパティが DB 上のテーブルでは主キーとして設定されます。int 型の場合は自動的に IDENTITY になりますので注意してください。

また、Name という名前のプロパティは、プロパティに付与した型 string と属性 Required(...), StringLength(5, ...) の設定により、DB のテーブルには Name という名前の nvarchar(5)、NULL 不許可のフィールドとして生成されます。 (参考にした @IT の記事に "null を許容する値型の列は、null 許容型で指定する" と書いてあったんですが、Required(...) 属性の設定が優先されるようです)

特に重要なのが、Parent クラスの中で virtual として宣言されている Chidren プロパティです。

これは「ナビゲーションプロパティ」と呼ばれるもので、上に紹介した @IT の記事によると "エンティティ上で virtual キーワードの付いたプロパティは、それが遅延ロードされることを表す。つまり、明示的に該当するプロパティにアクセスするまで、参照先の値はデータベースから取得されない" というものだそうです。

逆に言えば、この記事の例では、プログラムで Parent クラスのナビゲーションプロパティ Children にアクセスすれば、データベースの Children テーブルから関連するレコードを取得できるということになります。

上記の Model をベースに Entity Framework Code First の機能を利用して SQL Server のデータベースに生成されるテーブルの構成は以下の画像(SQL Server Management Studio で見たもの)に示す通りです。

生成されたテーブル

aspnet-Mvc4App2-20140721201705 というデータベースは、Visual Studio の「MVC4 のインターネットアプリケーション」テンプレートでアプリケーションを作成して、ユーザー登録すると自動的に作成されるものです。(開発環境に SQL Server Express がインストールされている場合)

Children テーブルの Parent_Id という外部キーフィールドは、Parent クラスに Children というナビゲーションプロパティを定義したことによって、フレームワークが自動的に作った NULL を許容する外部キーフィールドです。Parents テーブルの Id フィールドと外部キー制約を持っています。

この記事の Child クラスには外部キープロパティを定義していませんが、Microsoft の文書「Code First 規約」の「リレーションシップ規約」セクションには "型には、ナビゲーションプロパティに加え、依存オブジェクトを表す外部キーのプロパティを追加することをお勧めします" と書いてあります。理由は、連鎖削除を設定するか否かをコントロールするためのようです。

2016/9/12 追記:
Microsoft の文書「Code First 規約」に従って、Child クラスにナビゲーションプロパティと外部キープロパティを定義するとどのような影響があるかを別の記事「Code First で外部キープロパティの定義」に書きました。外部キープロパティを int 型にしたので、外部キーフィールドはこの記事と違って NULL 不可になります。詳しくは記事を見てください。

「親の名1」「親の名2」という 2 つのフィールドは、ParentChildInitializer クラスの Seed メソッドにより初期値として設定されたものです。

Global.asax

イニシャライザ ParentChildInitializer クラスは、Global.asax の Application_Start メソッドでアプリケーションに登録します。具体例は以下の通りです。

public class MvcApplication : System.Web.HttpApplication
{
  protected void Application_Start()
  {
    AreaRegistration.RegisterAllAreas();

    // ・・・中略・・・

    Database.SetInitializer<ParentChildContext>(
                           new ParentChildInitializer()); 
  }
}

ここまで済んだら、Visual Studio の[ビルド(B)]⇒[ソリューションのビルド(B)]でソリューションをビルドしておきましょう。そうしておかないと、以降の手順でモデル・クラスなどが認識されません。

実は、ここまででは、まだ SQL Server の DB にはテーブルは作成されていません。Controller と View を作って、Controller のアクションメソッドを呼び出す必要があります。

Controller と View はスキャフォールディング機能で自動的に生成できます(詳しくは上に紹介した @IT の記事を見てください)。以下に、一覧を表示する Index の部分のみ書いておきます。

Controller

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Mvc4App2.Models;

namespace Mvc4App2.Controllers
{
  public class ParentChildController : Controller
  {
    private ParentChildContext db = new ParentChildContext();

    //
    // GET: /ParentChild/

    public ActionResult Index()
    {
      return View(db.Parents.ToList());
    }

    // ・・・中略・・・

  }
}

View

@model IEnumerable<Mvc4App2.Models.Parent>

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

<h2>Index</h2>
<p>
  @Html.ActionLink("Create New", "Create")
</p>
<table>
  <tr>
    <th>
      @Html.DisplayNameFor(model => model.Id)
    </th>
    <th>
      @Html.DisplayNameFor(model => model.Name)
    </th>
        
    <th></th>
  </tr>

  @foreach (var item in Model) {
    <tr>
      <td>
        @Html.DisplayFor(modelItem => item.Id)
      </td>
      <td>
        @Html.DisplayFor(modelItem => item.Name)
      </td>
      <td>
        @Html.ActionLink("Edit", "Edit", 
            new { id=item.Id }) |
        @Html.ActionLink("Details", "Details", 
            new { id=item.Id }) |
        @Html.ActionLink("Delete", "Delete", 
            new { id=item.Id })
      </td>
    </tr>
  }
</table>

一番上の画像が、上記の Controller と View を使って表示したものです。

Tags:

MVC

ジェネリックハンドラー

by WebSurfer 2014年12月16日 14:42

Web サイトプロジェクトでジェネリックハンドラー(.ashx)を作成する際、コードビハインド形式では作成できないのですが、それは何故かについて書きます。(どうでもいい話かもしれませんが)

ジェネリックハンドラーの作成

上の画像は Visual Studio 2010 のソリューションエクスプローラーから「新しい項目の追加」ダイアログを開いてジェネリックハンドラーを追加しようとしているところです。[別のファイルにコードを書き込む(P)]のオプションがグレーアウトされて選択できないのがわかるでしょうか? そのまま作業を進めると、.ashx ファイルのみが生成されます。

一方、Web アプリケーションプロジェクトの場合は[別のファイルにコードを書き込む(P)]というようなオプションはそもそも表示されません(ジェネリックハンドラに限らず Web フォームなど他の項目も同様ですが)。自動的にコードビハインド形式になります(.ashx ファイルと .ashx.cs ファイルが作成されます)。

(注:Web アプリケーションプロジェクトと Web サイトプロジェクト の違いについては MSDN ライブラリの記事をリンクしたのでそこを見てください)

Web サイトプロジェクトでコードビハインド形式で作成できない理由が書いてある Microsoft の文書は見つけられなかったのですが、MSDN ライブラリ HTTP ハンドラの概要 のページの「ファイル名拡張子の作成」のセクションの以下の記述が関係ありそうです。

"ファイル名拡張子 .ashx に対応する HTTP ハンドラクラスを作成した場合、ハンドラは IIS と ASP.NET に自動的に登録されます。"

"ハンドラに対応するカスタムファイル名拡張子を作成する場合は、この拡張子を IIS と ASP.NET に明示的に登録する必要があります。"

引用文の前者がこの記事で言うジェネリックハンドラーのことで、HTTP ハンドラ本体(IHttpHandler を継承するクラス)とハンドラマッピング(@ WebHandler ディレクティブと Class 属性定義)を同一ファイル内に実装して .ashx という拡張子を付与し、IIS と ASP.NET に自動的に登録されるようにしたものです。

引用文の後者は、一般的な HTTP ハンドラのことで、HTTP ハンドラ本体を普通のクラスファイルに実装し、ソースコードのまま App_Code フォルダに置くか、コンパイルして dll を作って Bin フォルダに配置し、web.config にハンドラマッピングを定義するということになります。

つまり、ジェネリックハンドラーは、いちいち web.config にハンドラマッピングの定義を書かなくても、例えば <img src=handler.ashx ... /> のようにするだけで簡単に使えます。

コードビハインド形式にする(.ashx ファイルと .ashx.cs ファイルに分ける)のは取り扱いが面倒になるだなので、Web サイトプロジェクトでジェネリックハンドラーをコードビハインド形式で作成するオプションがないのだと思います。

Web アプリケーションプロジェクトではコード部分を分けざるを得ませので、選択の余地なく .ashx ファイルと .ashx.cs ファイルが分けて生成されます。ただし、Visual Studio からは .ashx.cs ファイルしか開けません(ファイルは存在しますが Visual Studio 上では開けません)。


以下は余談ですが、「コードビハインド」という呼び方について一言・・・

本来、HTML デザインブロックの背後にある ASP.NET 処理ブロック(.aspx.cs, .master.cs, .ascx.cs などに定義されるクラス)をコードビハンドと呼ぶようです。なので、.ashx ファイルの場合はコードビハンドという呼び方ではなく「HTTP ハンドラ本体」と言う方が適切かもしれません。

また、@ Page ディレクティブの CodeBehind 属性の説明を読むと "この属性は、以前のバージョンの ASP.NET との互換性を保持して、分離コード機能を実装するために用意されています。ASP.NET Version 2.0 では、代わりに CodeFile 属性を使用してソース ファイルの名前を指定すると同時に、Inherits 属性を使用してクラスの完全修飾名を指定します" とのことです。

@ WebHandler ディレクティブにも CodeBehind 属性はありますが、MSDN ライブラリによると "この属性は実行時には使用されません。この属性は、以前のバージョンの ASP.NET と互換性を保つために含まれています" とのことです。

なので「コードビハインド」という呼び方自体が古いのかもしれません。

Tags:

ASP.NET

About this blog

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

Calendar

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

View posts in large calendar