はじめに
「空のプロジェクトからASP.NET MVC5 EntityFramework6 for Oracleを試す(登録・更新・削除)」の続きで既にテーブルがある既存アプリケーションをASP.NET MVCで再構築する場合、どうやればいいのか。
MVCの特徴の1つでもあるORマッパーは使用せずに、SQL文を直接実行したい。また、接続文字列を動的に変更したい。
会社では運用チームに属していて、各県にある30近い工場の運用作業を行っている。導入作業などでは、稼働しているかどうかを実績データを参照し、しばらく実績データの更新状態が無いことを確認してから作業をしている。
各工場の接続文字列を動的に変更し、実績データの最終更新日時を取得を行いたい。よって、ORマッパーは不要でありSQL文でデータを取得する。
【2018/03/25追記】
EntityFramework でスキーマが切り替わらない - MSDNフォーラム
この記事のサンプルは、ホストは違うもののスキーマ名は同一となっている。
上記質問でOnModelCreatingメソッドのHasDefaultSchemaでスキーマ名を動的に変えれば出来るのはと思ったけど、OnModelCreatingメソッドは派生コンテキストの最初のインスタンスの作成時に一度だけ呼び出されるので、そこで変更してもキャッシュ使用されて動的にスキーマを変更しても切り替わらない。
質問がEF Core になっている中で回答にはEF6が含まれているので動的にスキーマを変更のヒントにはなるかと思います。
- How to implement IModelCacheKeyFactory in EF Core
- How to change database schema on runtime in EF7 or EF core
- Dynamically changing schema in Entity Framework Core
接続文字列の手動切替
方法1
Web.config に必要な分の接続文字列を記述する。
接続データとして、192.168.64.190と192.168.64.173の2つのホストを想定し、接続文字列(Context190とContext173)を2つ記述している。
<oracle.manageddataaccess.client>
<version number="*">
<dataSources>
<dataSource alias="XE190" descriptor="(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=192.168.64.190)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=XE))) "/>
<dataSource alias="XE173" descriptor="(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=192.168.64.173)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=XE))) "/>
</dataSources>
</version>
</oracle.manageddataaccess.client>
<connectionStrings>
<add name="Context190" providerName="Oracle.ManagedDataAccess.Client"
connectionString="User Id=test;Password=fuga;Data Source=XE190"/>
<add name="Context173" providerName="Oracle.ManagedDataAccess.Client"
connectionString="User Id=test;Password=fuga;Data Source=XE173"/>
</connectionStrings>
接続文字列を切り替えるために、StandardContext(string nameOrConnectionString)を追記します。
using System.Data.Common;
using System.Data.Entity;
namespace MvcBasic.Models
{
public class StandardContext : DbContext
{
public StandardContext(string nameOrConnectionString) : base(nameOrConnectionString)
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//スキーマを指定(デフォルトではdbo)
modelBuilder.HasDefaultSchema("TEST");
}
}
}
new StandardContext(接続文字列)の引数に接続文字列(Context190とContext176)のいずれかを指定し、SQL文で更新日時を取得してDataTable型に格納する。
※本来はコントローラーにモデルの内容を入れるべきではないが、これはサンプルなので。
using System.Web.Mvc;
using System.Data;
using System.Data.Common;
using MvcBasic.Models;
namespace MvcBasic.Controllers
{
public class LatestResultController : Controller
{
// アクションメソッド
public ActionResult List()
{
DataTable tb = new DataTable();
using (var context = new StandardContext("Context190")) // or Context173
{
DbProviderFactory factory = DbProviderFactories.GetFactory(context.Database.Connection);
using (var cmd = factory.CreateCommand())
{
cmd.CommandText = "SELECT MAX(update_time) AS update_time_max FROM RESULTDATA";
cmd.CommandType = CommandType.Text;
cmd.Connection = context.Database.Connection;
using (var adapter = factory.CreateDataAdapter())
{
adapter.SelectCommand = cmd;
adapter.Fill(tb);
}
}
}
return Content(tb.Rows[0]["update_time_max"].ToString());
}
}
}
方法2
Web.config は使用しない。
接続文字列を動的生成した後の接続用にStandardContext(DbConnection connection) を追記します。
using System.Data.Common;
using System.Data.Entity;
namespace MvcBasic.Models
{
public class StandardContext : DbContext
{
public StandardContext(DbConnection connection) : base(connection, true)
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//スキーマを指定(デフォルトではdbo)
modelBuilder.HasDefaultSchema("TEST");
}
}
}
接続文字列の違いは、ホストIPだけなので接続文字列を動的に生成して簡単に切り替えれるようにしておく。
※本来はコントローラーにモデルの内容を入れるべきではないが、これはサンプルなので。
using System.Web.Mvc;
using System.Data;
using System.Data.Common;
using MvcBasic.Models;
namespace MvcBasic.Controllers
{
public class LatestResultController : Controller
{
// アクションメソッド
public ActionResult List()
{
DataTable tb = new DataTable();
var connection = DbProviderFactories.GetFactory("Oracle.ManagedDataAccess.Client").CreateConnection();
connection.ConnectionString = String.Format("Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST={0})(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=gounn.WH))); User Id=test;Password=fuga", "192.168.64.190"); // or 192.168.64.173
using (var context = new StandardContext(connection))
{
DbProviderFactory factory = DbProviderFactories.GetFactory(context.Database.Connection);
using (var cmd = factory.CreateCommand())
{
cmd.CommandText = "SELECT MAX(update_time) AS update_time_max FROM RESULTDATA";
cmd.CommandType = CommandType.Text;
cmd.Connection = context.Database.Connection;
using (var adapter = factory.CreateDataAdapter())
{
adapter.SelectCommand = cmd;
adapter.Fill(tb);
}
}
}
return Content(tb.Rows[0]["update_time_max"].ToString());
}
}
}
ドロップダウンリストで接続文字列を切り替える
方法1のWeb.config に必要な分の接続文字列を設定した状態で作成していく。
ドロップダウンリストの選択肢の設定
方法1のstandardContext.cs に下記のクラスを追加する。
参照:ASP.NET MVC 5 でドロップダウンリストを使用する – ViewModel 編
// ドロップダウンリストの選択肢を表す ViewModel です。
public class FactoryViewModel
{
// 選択肢の値をセットします。
public string Value { get; set; }
// 選択肢として表示するテキストを取得するためのプロパティ。
public string DisplayText
{
get
{
return $"{this.Value}";
}
}
}
// ページに必要な情報を全て持つ ViewModel です。
// View はこの ViewModel から値を取得するようにしています。
public class DropDownListViewModel
{
// ドロップダウンリストで選択している値を取得または設定するためのプロパティです。
public string Factory { get; set; }
// ドロップダウンリストの選択肢をセットします。
// ※ ViewBag で代用も可能。
public IEnumerable<FactoryViewModel> FactoryOptions { get; set; }
}
アクションの設定
ドロップダウンリストのセットとPost後のアクションを設定する。
using System.Web.Mvc;
using System.Data;
using System.Data.Common;
using MvcBasic.Models;
namespace MvcBasic.Controllers
{
public class LatestResultController : Controller
{
public ActionResult List()
{
var vm = new DropDownListViewModel();
// ここでビューモデルに選択肢をセットしています。
vm.FactoryOptions = this.GetFactoryOptions();
return View(vm);
}
[HttpPost]
public ActionResult List(DropDownListViewModel vm)
{
// もう一度ビューモデルに選択肢をセットしています。
// Post されてこの箇所に来た時点で、
// FactoryOptions プロパティは null になっているので、
// 再度セットする必要があります。
vm.FactoryOptions = this.GetFactoryOptions();
DataTable tb = new DataTable();
using (var context = new StandardContext(vm.Factory))
{
DbProviderFactory factory = DbProviderFactories.GetFactory(context.Database.Connection);
using (var cmd = factory.CreateCommand())
{
cmd.CommandText = "SELECT MAX(update_time) AS update_time_max FROM RESULTDATA";
cmd.CommandType = CommandType.Text;
cmd.Connection = context.Database.Connection;
using (var adapter = factory.CreateDataAdapter())
{
adapter.SelectCommand = cmd;
adapter.Fill(tb);
}
}
}
ViewData["msg"] = String.Format("{0} {1}", "RESULTDATA", tb.Rows[0]["update_time_max"].ToString());
return View(vm);
}
// 工場一覧の選択肢を取得するためのメソッドです。
private IEnumerable<FactoryViewModel> GetFactoryOptions()
{
string[] sourceDatas = new string[] { "Context190", "Context173" };
return sourceDatas.Select(t => new FactoryViewModel() { Value = t });
}
}
}
ビューの設定
@model MvcBasic.Models.DropDownListViewModel
@using (Html.BeginForm())
{
<div class="form-horizontal">
<h4>実績データ - 最終更新日時取得</h4>
<hr />
<div class="form-group">
<div class="col-sm-10">
<label for="lblFactory">工場:</label>
@Html.DropDownListFor(
model => model.Factory,
new SelectList(Model.FactoryOptions, "Value", "DisplayText"),
"工場を選択",
new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.Factory, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<input type="submit" value="実績データ取得" class="btn btn-default" />
</div>
</div>
<hr />
<div>
@ViewData["msg"]
</div>
</div>
}
実行結果
今回は仕組みを理解しつつ簡易的に作成したものなので、実際には実績テーブル数も多くシステムごとにも振り分けたい。