カテゴリー
Microsoft

【ASP.NET MVC5】ちょっとしたお試しをするときに土台となるアプリの作り方チュートリアル

2016年8月16日追記: 本投稿のコードをアップいたしました。

追記: 【ASP.NET】【MVC5】お試しウェブアプリチュートリアルのエラーを解決 – oki2a24 で説明した通り、エラーを回避するために手順を一部変更しております。

サンプルアプリ概要

MVC 関係

  • スキャフォールディングで各テーブルのコントローラー、ビューをぱっと作って済ませる。
  • 認証なし

データベース関係

  • データベース名: ApplicationDb
  • テーブル
    • Parents
    • Children
    • Sexes ← 男、女が入っているだけのマスタテーブル
  • リレーション
    • Parents 1-n Children
    • Parents n-1 Sexes
    • Children n-1 Sexes
  • レコードから見るテーブルのリレーション表現は
    • Parents には Children 関係のカラムは無い。
    • Children は ParentsId カラムを持ち、親を指定する。
    • Parents と Children は SexId を持ち、性別を指定する。
    • Sexes には、他テーブル関係のカラムは無い。

それでは実際に作っていきましょう♪

プロジェクトの作成

  1. [ファイル] > [新規作成] > [プロジェクト] ← Ctrl + Shift + N
  2. [.NET Framework 4.6.1]、Visual C# の [ASP.NET Web アプリケーション]
    • プロジェクト名: Sample1
    • ソリューション名: Sample1
  3. テンプレートは [MVC]、チェックは無し、認証: [認証なし]

モデルの作成

Parents テーブルに対応する Parent クラス、Children テーブルに対応する Child クラス、Sexes テーブルに対応する Sex クラスを Model フォルダに作成します。

追記: Child.Birthday に、[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] を追加しました。次の投稿を元にしております♪

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace Sample1.Models
{
    public class Parent
    {
        public int Id { get; set; }
        [Required]
        public string Name { get; set; }
        public int SexId { get; set; }
        public virtual Sex Sex { get; set; }
        [Required]
        [DataType(DataType.EmailAddress)]
        public string Email { get; set; }
        public virtual ICollection<Child> Children { get; set; }
    }
}
using System;
using System.ComponentModel.DataAnnotations;

namespace Sample1.Models
{
    public class Child
    {
        public int Id { get; set; }
        [Required]
        public string Name { get; set; }
        public int SexId { get; set; }
        public virtual Sex Sex { get; set; }
        [Required]
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime Birthday { get; set; }
        [Required]
        public int ParentId { get; set; }
        public virtual Parent Parent { get; set; }
    }
}
using System.ComponentModel.DataAnnotations;

namespace Sample1.Models
{
    public class Sex
    {
        public int Id { get; set; }
        [Required]
        public string Name { get; set; }
    }
}

DbContext クラスの作成と、cascadeDelete の設定、Web.config への接続設定追加

ここの手順が、エラー回避のために追加となった箇所です。

以前であれば、この段階でスキャフォールディングによるコントローラーを追加します。そしてそのとき、データコンテキストクラス (DbContext クラス) を自動生成させておりました。ここの自動生成が、エラーにより行われなくなりました。

EntityFramework.ja パッケージのインストール

パッケージマネージャーコンソールから、次のパッケージをインストールし、DbContext クラスを扱えるようにいたします。

なお、EntityFramework.ja ではなく、EntityFramework でもよいのですけれども、日本語での開発ですので、EntityFramework.ja をインストールすることで両方共インストールされるようにいたしました。

Install-Package EntityFramework.ja

DbContext クラスの作成と、cascadeDelete の設定

データコンテキストクラスに次の OnModelCreating 関数を追加しています。これはモデルクラスに書ききれない、テーブルのリレーションを表すものですの♪

Sex テーブルは複数のテーブルから参照されるマスタであるため、cascadeDelete をオフにいたします。

using System.Data.Entity;

namespace Sample1.Models
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext() : base("name=ApplicationDbContext")
        {
        }

        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }

        public DbSet<Child> Children { get; set; }
        public DbSet<Parent> Parents { get; set; }
        public DbSet<Sex> Sexes { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Parent>()
                .HasRequired(c => c.Sex)
                .WithMany()
                .WillCascadeOnDelete(false);

            modelBuilder.Entity<Child>()
                .HasRequired(c => c.Sex)
                .WithMany()
                .WillCascadeOnDelete(false);
        }
    }
}

なお、この設定を書かなかった場合、Update-Database 時に次のようなエラーとなり次のマイグレーションが失敗いたしました><。

設定を書いて、Add-Migration Initial -Force などの操作でマイグレーションをやり直して成功することができました。

System.Data.SqlClient.SqlException (0x80131904): Introducing FOREIGN KEY constraint 'FK_dbo.Parents_dbo.Sexes_SexId' on table 'Parents' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.
   場所 System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   場所 System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   場所 System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   場所 System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   場所 System.Data.SqlClient.SqlCommand.RunExecuteNonQueryTds(String methodName, Boolean async, Int32 timeout, Boolean asyncWrite)
   場所 System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, String methodName, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite)
   場所 System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   場所 System.Data.Entity.Infrastructure.Interception.DbCommandDispatcher.<NonQuery>b__0(DbCommand t, DbCommandInterceptionContext`1 c)
   場所 System.Data.Entity.Infrastructure.Interception.InternalDispatcher`1.Dispatch[TTarget,TInterceptionContext,TResult](TTarget target, Func`3 operation, TInterceptionContext interceptionContext, Action`3 executing, Action`3 executed)
   場所 System.Data.Entity.Infrastructure.Interception.DbCommandDispatcher.NonQuery(DbCommand command, DbCommandInterceptionContext interceptionContext)
   場所 System.Data.Entity.Internal.InterceptableDbCommand.ExecuteNonQuery()
   場所 System.Data.Entity.Migrations.DbMigrator.ExecuteSql(MigrationStatement migrationStatement, DbConnection connection, DbTransaction transaction, DbInterceptionContext interceptionContext)
   場所 System.Data.Entity.Migrations.Infrastructure.MigratorLoggingDecorator.ExecuteSql(MigrationStatement migrationStatement, DbConnection connection, DbTransaction transaction, DbInterceptionContext interceptionContext)
   場所 System.Data.Entity.Migrations.DbMigrator.ExecuteStatementsInternal(IEnumerable`1 migrationStatements, DbConnection connection, DbTransaction transaction, DbInterceptionContext interceptionContext)
   場所 System.Data.Entity.Migrations.DbMigrator.ExecuteStatementsWithinTransaction(IEnumerable`1 migrationStatements, DbTransaction transaction, DbInterceptionContext interceptionContext)
   場所 System.Data.Entity.Migrations.DbMigrator.ExecuteStatementsWithinNewTransaction(IEnumerable`1 migrationStatements, DbConnection connection, DbInterceptionContext interceptionContext)
   場所 System.Data.Entity.Migrations.DbMigrator.ExecuteStatementsInternal(IEnumerable`1 migrationStatements, DbConnection connection, DbInterceptionContext interceptionContext)
   場所 System.Data.Entity.Migrations.DbMigrator.ExecuteStatementsInternal(IEnumerable`1 migrationStatements, DbConnection connection)
   場所 System.Data.Entity.Migrations.DbMigrator.<>c__DisplayClass30.<ExecuteStatements>b__2e()
   場所 System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.<>c__DisplayClass1.<Execute>b__0()
   場所 System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.Execute[TResult](Func`1 operation)
   場所 System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.Execute(Action operation)
   場所 System.Data.Entity.Migrations.DbMigrator.ExecuteStatements(IEnumerable`1 migrationStatements, DbTransaction existingTransaction)
   場所 System.Data.Entity.Migrations.DbMigrator.ExecuteStatements(IEnumerable`1 migrationStatements)
   場所 System.Data.Entity.Migrations.Infrastructure.MigratorBase.ExecuteStatements(IEnumerable`1 migrationStatements)
   場所 System.Data.Entity.Migrations.DbMigrator.ExecuteOperations(String migrationId, VersionedModel targetModel, IEnumerable`1 operations, IEnumerable`1 systemOperations, Boolean downgrading, Boolean auto)
   場所 System.Data.Entity.Migrations.DbMigrator.ApplyMigration(DbMigration migration, DbMigration lastMigration)
   場所 System.Data.Entity.Migrations.Infrastructure.MigratorLoggingDecorator.ApplyMigration(DbMigration migration, DbMigration lastMigration)
   場所 System.Data.Entity.Migrations.DbMigrator.Upgrade(IEnumerable`1 pendingMigrations, String targetMigrationId, String lastMigrationId)
   場所 System.Data.Entity.Migrations.Infrastructure.MigratorLoggingDecorator.Upgrade(IEnumerable`1 pendingMigrations, String targetMigrationId, String lastMigrationId)
   場所 System.Data.Entity.Migrations.DbMigrator.UpdateInternal(String targetMigration)
   場所 System.Data.Entity.Migrations.DbMigrator.<>c__DisplayClassc.<Update>b__b()
   場所 System.Data.Entity.Migrations.DbMigrator.EnsureDatabaseExists(Action mustSucceedToKeepDatabase)
   場所 System.Data.Entity.Migrations.Infrastructure.MigratorBase.EnsureDatabaseExists(Action mustSucceedToKeepDatabase)
   場所 System.Data.Entity.Migrations.DbMigrator.Update(String targetMigration)
   場所 System.Data.Entity.Migrations.Infrastructure.MigratorBase.Update(String targetMigration)
   場所 System.Data.Entity.Migrations.Design.ToolingFacade.UpdateRunner.Run()
   場所 System.AppDomain.DoCallBack(CrossAppDomainDelegate callBackDelegate)
   場所 System.AppDomain.DoCallBack(CrossAppDomainDelegate callBackDelegate)
   場所 System.Data.Entity.Migrations.Design.ToolingFacade.Run(BaseRunner runner)
   場所 System.Data.Entity.Migrations.Design.ToolingFacade.Update(String targetMigration, Boolean force)
   場所 System.Data.Entity.Migrations.UpdateDatabaseCommand.<>c__DisplayClass2.<.ctor>b__0()
   場所 System.Data.Entity.Migrations.MigrationsDomainCommand.Execute(Action command)
ClientConnectionId:e67808a6-d2e0-450c-b47b-b9efa62e99a1
Error Number: 1785、State: 0、Class: 16
Introducing FOREIGN KEY constraint 'FK_dbo.Parents_dbo.Sexes_SexId' on table 'Parents' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.

Web.config への接続設定追加

DbContext クラスのコンストラクタで public ApplicationDbContext() : base("name=ApplicationDbContext") と Web.config の connectionStrings の name を指定しております。

この内容に合うように、Web.config を編集いたします。

<configuration>
  <configSections>
    ... 略 ...
  </configSections>
  <connectionStrings>
    <add name="ApplicationDbContext" connectionString="Data Source=(localdb)\MSSQLLocalDB; Initial Catalog=ApplicationDb; Integrated Security=True; MultipleActiveResultSets=True; AttachDbFilename=|DataDirectory|ApplicationDb.mdf" providerName="System.Data.SqlClient" />
  </connectionStrings>

コントローラーのスキャフォールディング作成

ビルド (Ctrl + Shift + B) を行い、Parent、Child、Sex、それぞれのコントローラーを作成します。

「Entity Framework を使用した、ビューがある MVC 5 コントローラー」を選んで作成いたしました。

マイグレーションとレコードの挿入

パッケージマネージャーコンソールから、次のコマンドでマイグレーションを行います。

Enable-Migrations
Add-Migration Initial

自動でフォルダ、ファイルが作成されます。その中の Configuration.cs の Seed 関数内に、テーブルに挿入するレコードを C# で記述します。

ここで注意したいのが、なぜかファイル上書き保存が Shift-JIS で行われます。その影響で Update-Database でデータベースに挿入されるデータが文字化けしてしまいました><。文字化けを避けるために Configuration.cs は、ファイル > 名前をつけて保存 > 上書き保存の「▼」 > エンコード付きで保存 > エンコード: Unicode (UTF-8 シグネチャ付き) – コードページ 65001、で保存します。

  • Sexes.Id を Parent や Child で指定するときは、Sexes.Name から Sexes.Id を検索した結果を設定する。例えば、sexes.Single(s => s.Name == "女").Id と書く。
  • Parent に追加した Child には、ParentId を設定する必要が無い。自動的に正しく設定される。
  • 年月日は、DateTime.Parse("2010-02-01") などと設定する。

var sexes = new List<Sex>
{
new Sex { Name = "男" },
new Sex { Name = "女" }
};
sexes.ForEach(s => context.Sexes.AddOrUpdate(p => p.Name, s));
context.SaveChanges();

var parents = new List<Parent>
{
new Parent { Name = "青木篤志", SexId = sexes.Single(s => s.Name == "男").Id, Email = "aoki.atsushi@example.com" },
new Parent { Name = "井上郁夫", SexId = sexes.Single(s => s.Name == "男").Id, Email = "inoue.ikuo@example.com", Children = new List<Child>
{
new Child { Name = "井上恵美子", SexId = sexes.Single(s => s.Name == "女").Id, Birthday = DateTime.Parse("2015-01-16") }
}
},
new Parent { Name = "宇佐美景子", SexId = sexes.Single(s => s.Name == "女").Id, Email = "usami.keiko@example.com", Children = new List<Child>
{
new Child { Name = "宇佐美涼介", SexId = sexes.Single(s => s.Name == "男").Id, Birthday = DateTime.Parse("2010-02-01") },
new Child { Name = "宇佐美信介", SexId = sexes.Single(s => s.Name == "男").Id, Birthday = DateTime.Parse("2013-11-08") },
new Child { Name = "宇佐美純", SexId = sexes.Single(s => s.Name == "男").Id, Birthday = DateTime.Parse("2015-04-05") }
}
}
};
parents.ForEach(s => context.Parents.AddOrUpdate(p => p.Name, s));
context.SaveChanges();

マイグレーションで生成されたテーブルのコード、追記したレコードのコードを次のコマンドで反映します。

Update-Database -Verbose

ビューを編集してリンクを追加

アプリの動作確認時に URL を直に打ち込むのは面倒ですので、共通のレイアウトに記述しておきますの。

<ul class="nav navbar-nav">
<li>@Html.ActionLink("ホーム", "Index", "Home")</li>
<li>@Html.ActionLink("詳細", "About", "Home")</li>
<li>@Html.ActionLink("連絡先", "Contact", "Home")</li>
<li>@Html.ActionLink("親", "Index", "Parents")</li>
<li>@Html.ActionLink("子", "Index", "Children")</li>
<li>@Html.ActionLink("性別", "Index", "Sexes")</li>
</ul>

確認

デバッグなしで開始 (Ctrl + F5) して、完成したアプリを確認します。

完成アプリキャプチャ。Children コントローラーの Index アクション
完成アプリキャプチャ。Children コントローラーの Index アクション

  • 紐づく性別や Parent は、レコードの Id ではなく対応する値 (「男」や「親の Name」)が表示される。
  • Create や Edit でも、性別や Parent は Id ではなく対応する値をドロップダウンリストで選択する形となる。
  • Parent を削除すると、紐づく Child も自動的に削除される。
  • Child を削除しても、紐づく Parent は残ったまま。
  • Sex レコードを参照する Parent、Child のレコードが残っているかぎり、Sex のレコードは削除できない。エラーとなる。

    [SqlException (0x80131904): The DELETE statement conflicted with the REFERENCE constraint “FK_dbo.Children_dbo.Sexes_SexId”. The conflict occurred in database “Sample1Context-20151016212614”, table “dbo.Children”, column ‘SexId’.
    The statement has been terminated.]

  • Sex レコードがない状態で Parent レコードを登録しようとするとエラーとなる。

    [SqlException (0x80131904): The INSERT statement conflicted with the FOREIGN KEY constraint “FK_dbo.Parents_dbo.Sexes_SexId”. The conflict occurred in database “Sample1Context-20151016212614”, table “dbo.Sexes”, column ‘Id’.
    The statement has been terminated.]

  • Parent レコードがない状態で、Child レコードは登録できない。エラーにはならなかった。
  • すべてのレコードを削除してしまったら、Update-Database で DB のレコードを再登録できる。

おわりに

今回投稿した以外にも、ASP.NET MVC5 のチュートリアルとして、次のページが優れていると思います!

試験的に何かをやってみようとするとき、データベース構造が単純すぎたり、複雑過ぎたりしましたので、その中間あたりの構造を狙って、今回土台となるようなアプリのチュートリアルを作成いたしました。

もっと簡単にできると思っておりましたけれども、つまづきました><。

特に、「コントローラーのスキャフォールディング作成と、cascadeDelete の設定」でも例外表示を引用いたしましたけれども、Parent にも Child にもマスタとして Sex を参照するようにしたところ、エラーとなって随分と焦りました><。

ALTER TABLE [dbo].[Children] ADD CONSTRAINT [FK_dbo.Children_dbo.Sexes_SexId] FOREIGN KEY ([SexId]) REFERENCES [dbo].[Sexes] ([Id]) ON DELETE CASCADE
ALTER TABLE [dbo].[Parents] ADD CONSTRAINT [FK_dbo.Parents_dbo.Sexes_SexId] FOREIGN KEY ([SexId]) REFERENCES [dbo].[Sexes] ([Id]) ON DELETE CASCADE
System.Data.SqlClient.SqlException (0x80131904): Introducing FOREIGN KEY constraint ‘FK_dbo.Parents_dbo.Sexes_SexId’ on table ‘Parents’ may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.

次のページを見つけられ、解決することができてホッとしております。

以上です。

「【ASP.NET MVC5】ちょっとしたお試しをするときに土台となるアプリの作り方チュートリアル」への1件の返信

コメントを残す