ORMベンチマーク(RawDataAccessBencher)をAzure IaaSで実行してみた

はじめに(雑談的な・・・)

新規のシステム開発を行う際に、最近 何気に悩むのが「データアクセスのアーキテクチャ選択」だと思います。
昔々20年前くらいであれば「SQLをADO経由でゴリゴリ実行するぞ!」でOKだったのが、今では「素のADO.NET」から「Entity Frameworkのような重量級(?)のORM(Object Relational Mapper)」「軽さを追い求めて作られたMicro ORMであるDapper」などなど・・・多岐に渡ります。

.NET開発におけるORMとしてはマイクロソフト公式技術として Entity Framework がありますが、ver.1.0 リリース時からスピードの遅さが指摘されていて、なんだか「これを採用だ!」と言いにくい雰囲気が作られているようにも思います。
で、このEntity Frameworkも気が付けば フルセット.NET用は Ver.6、.NET Core用は Ver1.0 RTM となっています。

SQL隠ぺい型ORM」か「SQL明示定義型ORM」か

ORMの種類を大きく分けると、「Entity FrameworkのようなSQLを意識しない方針のORM」と「DapperやMyBatisのようなSQLを強く意識する方針のORM」があります。
私は データベースエンジニア ではなく ソフトウェアエンジニア の人間なので、前者の世界の方が個人的には好みであり、理想の世界だと思っています。
ソフトウェアを開発するにあたり、アプリケーションロジックはC#Rubyのようなオブジェクト指向言語で記述し、「データベースアクセスにはSQLという、リレーショナルデータを操作するための手続き型言語を利用する」というのはソフトウェアアーキテクチャとしては煩雑なのです。SQLが書きたいわけではなく、欲しいデータを欲しいフォーマットで取得したいだけなのだから、そしてその型はドメイン分析の結果得られたクラスオブジェクト型であってほしいのです。
SQL世界を実現するのが、Entity Frameworkであり、LINQという言語ですよね。

とはいえ、高いパフォーマンス、高負荷状態でも高速なデータ取得をリレーショナルデータベースに求めると、やはりチューニングされたSQLを実行する事が最も高速に動作するというのも事実。そして、結構こういうケースが多いと思います。
なので、「データアクセスのアーキテクチャ選択」が難しいのだと思います。
選択肢としては1つの技術を選択するだけでなく、生産性重視のORM+速度重視の別技術、など、1システム内でも機能毎に利用する技術を別々にするのも選択肢だと思います。

ベンチマークしてみた

で、改めて各種データアクセス技術のベンチマークを取ってみよう、と思いました。
利用したのは「RawDataAccessBencher」。GitHub上で公開されている 素のADO.NET および 各種ORM でのクエリーパフォーマンスを測定するベンチマークプログラムです。
Githubページ上でも、ベンチマーク結果が公開されているので、自分で動かさなくてもベンチマーク結果を参照することはできますが、ここでは改めてAzure IaaS上でベンチマークを取ってみました。

Azure IaaSは「Standard A6 (4 コア、28 GB メモリ) / Windows Server 2016 / SQL Server 2016 RTM」の環境を利用しました。
Azure IaaSを新規に作っているので、まっさらな ベンチマークには最適な環境だと思います。

データベースの準備

RawDataAccessBencherは、SQL Serverサンプルデータベースである「AdventureWorks」を利用してベンチマーク測定を行います。
AdventureWorksデータベースは、以前はSQL Serverインストーラに標準で付属していましたが、2012あたり(?)から付属しなくなりました。
以下のURLからダウンロードする事が出来ます。

Microsoft SQL Server Product Samples: Database - Downloads

f:id:daigo-knowlbo:20161113163933p:plain

SQL Management Studioを起動し、Databaseノードでマウス右ボタンメニュー「Restore Database...」を選択します。

f:id:daigo-knowlbo:20161113184905p:plain

SourceからDeviceを選択し、ダウンロードした「AdventureWorks2014.bak」ファイルを選択して、OKボタンをクリックします。

f:id:daigo-knowlbo:20161113185013p:plain

以上でSQL Server 2016上に AdventureWorks2014 サンプルデータベースが復元されました。

RawDataAccessBencherのビルド

Github(https://github.com/FransBouma/RawDataAccessBencher)からソリューションをCloneするかzip downloadします。
Visual Studio 2015 で RawBencher.sln をオープンします。
構成を Release に設定します。

!! 注意点 !!

2つほど注意点があります。
【1点目】
RawDataAccessBencherは、データベースへの接続文字列をApp.config(RawBencher.exe.config)に保持しますが、デフォルトでは利用データベース名は「AdventureWorks」となっています。
しかし、先程リストアしたデータベースは AdventureWorks2014 というデータベース名となっています。その為、接続データベース名の修正が必要になります。
App.config(RawBencher.exe.config)の以下の箇所のコメントを解除して有効にします。

<section name="sqlServerCatalogNameOverwrites" type="System.Configuration.NameValueSectionHandler" />

続けて、以下の箇所のコメントを解除して、更にVlue値を以下のように AdventureWorks2014 とします。

<sqlServerCatalogNameOverwrites>
  <add key="AdventureWorks" value="AdventureWorks2014" />
</sqlServerCatalogNameOverwrites>

更に以下の箇所の data source 値を適時マシン名に、また、initial catalog 値を AdventureWorks2014 に変更します。

<add name="AdventureWorks.ConnectionString.SQL Server (SqlClient)" connectionString="data source=DESKTOP-LFQS0ND;initial catalog=AdventureWorks2014;integrated security=SSPI;persist security info=False;packet size=4096" providerName="System.Data.SqlClient" />
<add name="EF.ConnectionString.SQL Server (SqlClient)" connectionString="metadata=res://*/AW.csdl|res://*/AW.ssdl|res://*/AW.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=DESKTOP-LFQS0ND;initial catalog=AdventureWorks2014;integrated security=SSPI;persist security info=False;packet size=4096&quot;" providerName="System.Data.EntityClient" />

【2点目】
「RawBencher\OriginalController.cs」ソースコード内に1箇所 AdventureWorks がハードコーディングされています。
ソースの以下の箇所を修正します。

KeysForIndividualFetches = conn.Query<int>("select top {=count} SalesOrderId from AdventureWorks.Sales.SalesOrderHeader order by SalesOrderNumber", new { count = IndividualKeysAmount }).AsList();
↓↓↓ 修正 ↓↓↓
KeysForIndividualFetches = conn.Query<int>("select top {=count} SalesOrderId from AdventureWorks2014.Sales.SalesOrderHeader order by SalesOrderNumber", new { count = IndividualKeysAmount }).AsList();

以上でビルド準備完了です。
Release構成でビルドを行い、作成された「bin\Releaseフォルダ」をAzure IaaS環境にコピーします。

ベンチマーク実行

一応、SQL Serverをクリアな状態にする為に、以下のコマンドをSQL Serverに対して実行します。

DBCC DROPCLEANBUFFERS;
DBCC FREEPROCCACHE;

コマンドプロンプトを開き、先程ビルド / IaaSにコピーした RawBencher.exe を実行します。

RawBencher.exe /a > Result.txt

「/a」パラメータを付与します。また「>」により標準出力結果を result.txt ファイルに出力するようにします。 (/a オプションを付与しないと、処理終了後にユーザー入力待ちが行われてしまいます。処理実行後、結果をファイル出力しそのまま自動的にプログラム実行は完了したい為に /a オプションを利用しています。)

結果

結果は以下の通りです。

ベンチマーク結果をダウンロード

結果からログ出力以外の主要な結果を抽出したものは以下の通り

Results per framework. Values are given as: 'mean (standard deviation)'
==============================================================================
Non-change tracking fetches, set fetches (25 runs), no caching
------------------------------------------------------------------------------
Handcoded materializer using DbDataReader                            : 319.68ms (9.93ms)    Enum: 3.93ms (0.39ms)
Handcoded materializer using DbDataReader (GetValues(array), boxing) : 351.40ms (11.97ms)   Enum: 3.82ms (0.43ms)
LINQ to DB v1.0.7.3 (v1.0.7.3) (normal)                              : 370.69ms (11.33ms)   Enum: 2.90ms (0.39ms)
Raw DbDataReader materializer using object arrays                    : 372.05ms (24.42ms)   Enum: 6.66ms (0.44ms)
LINQ to DB v1.0.7.3 (v1.0.7.3) (compiled)                            : 378.16ms (15.66ms)   Enum: 2.92ms (0.40ms)
PetaPoco Fast v4.0.3                                                 : 381.83ms (7.74ms)    Enum: 3.97ms (0.47ms)
LLBLGen Pro v5.0.0.0 (v5.0.4), Poco typed view with QuerySpec        : 430.82ms (10.68ms)   Enum: 3.12ms (0.40ms)
PetaPoco v4.0.3                                                      : 438.12ms (12.51ms)   Enum: 4.04ms (0.42ms)
LLBLGen Pro v5.0.0.0 (v5.0.4), Poco typed view with Linq             : 448.98ms (25.86ms)   Enum: 2.93ms (0.48ms)
Dapper v1.50.0.0                                                     : 470.63ms (24.97ms)   Enum: 4.10ms (1.20ms)
ServiceStack OrmLite v4.0.60.0 (v4.0.60.0)                           : 473.27ms (18.74ms)   Enum: 3.75ms (0.42ms)
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 495.84ms (17.27ms)   Enum: 3.48ms (1.12ms)
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 508.53ms (14.41ms)   Enum: 3.37ms (0.45ms)
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 539.37ms (19.22ms)   Enum: 3.17ms (0.36ms)
LLBLGen Pro v5.0.0.0 (v5.0.4), DataTable based TypedView             : 903.03ms (19.57ms)   Enum: 12.62ms (2.57ms)
Massive using dynamic class                                          : 1,294.43ms (21.67ms) Enum: 54.08ms (7.80ms)
Oak.DynamicDb using dynamic Dto class                                : 1,619.00ms (86.53ms) Enum: 357.62ms (81.74ms)

Change tracking fetches, set fetches (25 runs), no caching
------------------------------------------------------------------------------
DataTable, using DbDataAdapter                                       : 594.19ms (20.67ms)   Enum: 77.41ms (4.93ms)
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 706.63ms (30.22ms)   Enum: 3.65ms (0.40ms)
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 787.29ms (37.16ms)   Enum: 27.01ms (1.80ms)
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 1,008.00ms (19.90ms) Enum: 3.76ms (0.34ms)
Oak.DynamicDb using typed dynamic class                              : 1,596.94ms (55.54ms) Enum: 2,268.00ms (101.37ms)
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 6,363.24ms (76.03ms) Enum: 5.31ms (0.64ms)
NHibernate v4.0.0.4000 (v4.0.4.4000)                                 : 6,473.33ms (128.28ms)    Enum: 5.47ms (1.10ms)

Non-change tracking individual fetches (100 elements, 25 runs), no caching
------------------------------------------------------------------------------
Handcoded materializer using DbDataReader                            : 0.28ms (0.02ms) per individual fetch
Handcoded materializer using DbDataReader (GetValues(array), boxing) : 0.28ms (0.02ms) per individual fetch
Dapper v1.50.0.0                                                     : 0.40ms (0.05ms) per individual fetch
ServiceStack OrmLite v4.0.60.0 (v4.0.60.0)                           : 0.41ms (0.04ms) per individual fetch
Oak.DynamicDb using dynamic Dto class                                : 0.51ms (0.06ms) per individual fetch
Raw DbDataReader materializer using object arrays                    : 0.53ms (0.06ms) per individual fetch
LINQ to DB v1.0.7.3 (v1.0.7.3) (compiled)                            : 0.78ms (0.06ms) per individual fetch
PetaPoco Fast v4.0.3                                                 : 0.82ms (0.04ms) per individual fetch
Massive using dynamic class                                          : 0.82ms (0.14ms) per individual fetch
LINQ to DB v1.0.7.3 (v1.0.7.3) (normal)                              : 0.86ms (0.03ms) per individual fetch
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 0.89ms (0.04ms) per individual fetch
LLBLGen Pro v5.0.0.0 (v5.0.4), Poco typed view with QuerySpec        : 1.02ms (0.04ms) per individual fetch
LLBLGen Pro v5.0.0.0 (v5.0.4), DataTable based TypedView             : 1.29ms (0.05ms) per individual fetch
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 1.30ms (0.04ms) per individual fetch
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 3.39ms (0.12ms) per individual fetch
LLBLGen Pro v5.0.0.0 (v5.0.4), Poco typed view with Linq             : 3.58ms (0.09ms) per individual fetch
PetaPoco v4.0.3                                                      : 8.72ms (0.14ms) per individual fetch

Change tracking individual fetches (100 elements, 25 runs), no caching
------------------------------------------------------------------------------
DataTable, using DbDataAdapter                                       : 0.51ms (0.09ms) per individual fetch
Oak.DynamicDb using typed dynamic class                              : 0.58ms (0.04ms) per individual fetch
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 0.89ms (0.06ms) per individual fetch
NHibernate v4.0.0.4000 (v4.0.4.4000)                                 : 1.02ms (0.05ms) per individual fetch
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 1.03ms (0.08ms) per individual fetch
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 1.47ms (0.04ms) per individual fetch
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 3.64ms (0.13ms) per individual fetch

Change tracking fetches, eager load fetches, 3-node split graph, 1000 root elements (25 runs), no caching
------------------------------------------------------------------------------
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 177.92ms (10.03ms)
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 227.49ms (10.30ms)
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 279.44ms (18.73ms)
NHibernate v4.0.0.4000 (v4.0.4.4000)                                 : 939.34ms (18.69ms)
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 962.27ms (18.81ms)

Async change tracking fetches, eager load fetches, 3-node split graph, 1000 root elements (25 runs), no caching
------------------------------------------------------------------------------
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 226.60ms (8.89ms)
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 775.05ms (18.05ms)
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 1,055.69ms (25.01ms)

Change tracking fetches, set fetches (25 runs), caching
------------------------------------------------------------------------------
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 360.75ms (139.12ms)  Enum: 27.46ms (4.68ms)

Change tracking individual fetches (100 elements, 25 runs), caching
------------------------------------------------------------------------------
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 0.61ms (0.15ms) per individual fetch

分析とパフォーマンスの受け入れ具合は、各システムにより異なると思いますが、Entity Framework Coreが なかなかいい成績だったのではないかと思っています。
また、数年前に比べて Dapper > Entity Framework の差が縮まっているのではないかなぁ・・という印象を持ちました。
加えて重要なのは、このベンチマークはクエリーのみで更新系は測定されていません。Entity Frameworkは特に更新系パフォーマンスに懸念点が多く挙げられていたので、最新版で改めて別途ベンチマーク測定を行ってみようとは思っています。

次回のブログ更新は、Entity Framework Coreにも対応している Dapper をいじってみようかと思います。

.NET Coreでマルチプロジェクト構成のソリューションを作る

.NET Coreに限らないお話ですが・・・本ブログ 及び 各所の技術解説では単一プロジェクト構成が取られることが多いです。
これは「対象解説において”スポットを当てる技術”以外の箇所の複雑さを省く」為です。

しかし、実際のシステム開発においては、「単一プロジェクト構成」ではなく「複数プロジェクト構成」が取られることがほとんどです。
例えば、レイヤー化アーキテクチャによる分離により、「Webモジュール(DLL)」「ドメインモジュール(DLL)」「Repositoryモジュール(DLL)」のようにモジュール分割を行います。この場合の”各モジュール”が、実装ソースレベルでは”各プロジェクト”になります。

本投稿では.NET Coreにおいてマルチプロジェクト構成を実現する方法について説明します。

環境

さて、本投稿の検証環境は以下の通りです。

dotnet --info

f:id:daigo-knowlbo:20161109013805p:plain

OSは Mac(OSx) 、.NET Core SDKは 1.0.0-preview2-1-003155 となります。

ソリューション・プロジェクトの構成

以下のソリューション・プロジェクトの構成を前提とします。

./MultiProjSol  
    --MyLib   ※ライブラリプロジェクト  
    --Myonsole  ※コンソールアプリケーションプロジェクト(MyLib内の実装クラスを参照利用する)  

つまり「MyConsole と MyLib」の2つのプロジェクトから構成されるソリューションです。
MyConsole は MyLib内の実装を参照(利用)します。

ソリューションディレクトリの作成

本投稿では、以下のディレクトリをソリューションディレクトリとします。

/Users/daigo/projects/MultiProjSol

MyLibプロジェクトの作成

MyLibディレクトリを作成し、dotnet new -t lib コマンドでライブラリプロジェクトを作成します。

cd /Users/daigo/projects/MultiProjSol
mkdir MyLib
cd MyLib
dotnet new -t lib

上記コマンド実行後のMyLibディレクトリの内容は以下の通りです。

ls -l
total 16
-rwxr--r--  1 daigo  staff  135 10  6 13:55 Library.cs
-rwxr--r--  1 daigo  staff  244 10 14 09:48 project.json

自動生成された Library.cs を以下のように修正します。

using System;

namespace MyLib
{
    public class EchoMania
    {
        public string Accost(string message)
        {    
            return "I'm 'Echo Mania'! Your message is '" + message + "'";
        }
    }
}

MyLib名前空間にEchoManiaクラスを実装しました。
Accostメソッドは、引数で渡されたmessageをエコーバックします。

MyConsoleプロジェクトの作成

MyConsoleディレクトリを作成し、dotnet new コマンドでコンソールアプリケーションプロジェクトを作成します。

cd /Users/daigo/projects/MultiProjSol
mkdir MyConsole
cd MyConsole
dotnet new

上記コマンド実行後の MyConsoleディレクトリ の内容は以下の通りです。

ls -l
total 16
-rwxr--r--  1 daigo  staff  202 10  6 13:55 Program.cs
-rwxr--r--  1 daigo  staff  367 10 14 09:48 project.json

Program.csを以下のように修正します。

using System;
using MyLib;

namespace MyConsole
{
    public class Program
    {
        public static void Main(string[] args)
        {
            EchoMania mania = new EchoMania();
            Console.WriteLine(mania.Accost("Hello World!"));
        }
    }
}

MyLib名前空間のEchoManiaをインスタンス化し、Accost()メソッドを呼び出します。
Accost()メソッドの戻り値の値をコンソールに出力します。

また、MyLibを利用する為に「project.json」の依存関係設定「dependencies」に対して追記を行います。
修正後の project.json は以下の通りです。

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {
    "MyLib": "*"
  },
  "frameworks": {
    "netcoreapp1.1": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.1.0-preview1-001100-00"
        }
      },
      "imports": "dnxcore50"
    }
  }
}

global.jsonを作成

「global.jsonファイルの作成」が、マルチプロジェクトを実現する為のキーポイントになります。
ソリューションのルートディレクトリ、つまり本サンプルでは「/Users/daigo/projects/MultiProjSol」に「global.json」ファイルを作成します。
global.json内の「projectsキー」にソリューションを構成する各プロジェクトパスを記述します。
以下が global.json になります。

{
    "projects":[
        "MyConsole",
        "MyLib"
    ]
}

dotnet restoreを実行

ソリューションディレクトリ(global.jsonの保存ディレクトリ)において、 dotnet restore を実行します。

dotnet restore

パッケージのリストアが行われます。

f:id:daigo-knowlbo:20161109020602p:plain

実行

ではスタートアッププロジェクトである「MyConsoleディレクトリ」に移動し、 dotnet run を実行します。

dotnet run

f:id:daigo-knowlbo:20161109024112p:plain

MyLibプロジェクトに実装した EchoManiaクラス を利用する MyConsoleアプリケーションがうまく動作しました!

本投稿では利用していませんが、Visual Studio Codeを組み合わせて利用する事で、実践的な .NET Core アプリケーションの実装は十分に可能となります。

.NET Core(Entity Framework Core)でCode Firstする

Entity Framework Core 1.1 Preview1でのCode First(コード・ファースト)による開発(というか、まず初めにC#でモデルクラスを定義。モデルクラスからデータベース定義を自動生成の流れ)について見ていきたいと思います。

「コード・ファースト」という言葉については、ここでは詳細は触れません(ググればいっぱい出てきますよね・・・)。

簡単にいうとソフトウェアを開発するにあたり、「リレーショナルデータベース設計ありき」ではなくて「作成するソフトウェアの”ドメイン領域の分析”に注力し、データベースはモデルのあるタイミングにおけるシリアライズ結果を保存する入れ物」的な考え方が基本となっていると思います。
リレーショナルデータベースとドメインオブジェクトは、その概念の相違から「インピーダンス・ミスマッチ」が発生することは必然であり、ソフトウェア側の人間としては、ドメイン領域のオブジェクトモデリングに集中すべき、という思想ですね。

とはいえ、リレーショナルデータベース固有の「高パフォーマンスを発揮する為のテーブル設計やSQLコマンド」のような概念もあり、一概にドメイン分析至上主義な設計ばかりできるわけではないことも事実です。

環境

まず本投稿における検証環境ですが・・・Mac(OSx)で dotnet --infoの結果は以下の通りです。

ryuichi:efCoreExample daigo$ dotnet --info
.NET Command Line Tools (1.0.0-preview2-1-003155)

Product Information:
 Version:            1.0.0-preview2-1-003155
 Commit SHA-1 hash:  d7b0190bd4

Runtime Environment:
 OS Name:     Mac OS X
 OS Version:  10.12
 OS Platform: Darwin
 RID:         osx.10.12-x64
ryuichi:efCoreExample daigo$ 

つまり、.NET Core 1.1 Preview1(SDK 1.0.0Preview2.1)となります。

.NET Core プロジェクトの作成

.NET Coreプロジェクトを作成します。ここでは単純なコンソールアプリケーションを作成します。
任意のフォルダ(ここでは /Users/daigo/projects/efCoreExample )にプロジェクトを作成します。

dotnet new

ls -lの結果は以下となります(自動生成されたソース一覧)。

-rwxr--r--  1 daigo  staff  202 10  6 13:55 Program.cs
-rwxr--r--  1 daigo  staff  367 10 14 09:48 project.json

モデルクラスの作成

モデルクラスを作成します。
ここでは、Compeny / Employee の2つのモデルクラスを定義します。
実装は以下の通りです。

// Company.cs
using System.Collections.Generic;

namespace EfCoreExample.Models
{
    public class Company 
    {
        public int id { get; set; }
        public string Name { get; set; }
        public int Capital { get; set; }
        public List<Employee> Employees { get; set; }
    }
}
// Employee.cs
namespace EfCoreExample.Models
{
    public class Employee 
    {
        public int id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}

※モデルクラスにはPrimaryKeyを用意する必要があります。Entity Frameworkの作法として「id」「Id」もしくは「【モデル名】Id」という名称のプロパティが存在した場合、これがPrimaryKeyであると自動で判断されます。DataAnnotationで [key] を明示的に指定する方法もあります。

DbContextの作成

Company / Employeeをデータベースに出し入れするためのDbContextクラスを定義します。
OnConfiguring()メソッド中でSQL Serverへの接続文字列を設定していますが、ここでの接続先は私の Azure SQL Database としています。

// CoreExampleContext.cs
using Microsoft.EntityFrameworkCore;

namespace EfCoreExample.Models
{
    public class CoreExampleContext : DbContext
    {
        public DbSet<Company> Company { get; set; }
        public DbSet<Employee> Employee { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=tcp:rdexampledb1svr.database.windows.net,1433;Initial Catalog=RdExampleDb1;Persist Security Info=False;User ID=[user];Password=[password];MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;");
        }
    }
}

project.jsonの修正

モデルクラスからコード・ファーストでSQL Serverデータベーステーブルへリバースを行うために、Entity Framework Coreのツール及びSQL Serverデータプロバイダが必要になります。これらをproject.jsonに dependencies 及び tools として明示的に参照宣言する必要があります。
いかが修正を加えた project.json です。

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {
    "Microsoft.EntityFrameworkCore.Design":{
      "type":"build",
      "version": "1.0.1"
    },
    "Microsoft.EntityFrameworkCore.SqlServer": "1.0.1"
  },
  "tools": {
    "Microsoft.EntityFrameworkCore.Tools.DotNet": "1.0.0-preview3-final"
  },
  "frameworks": {
    "netcoreapp1.1": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.1.0-preview1-001100-00"
        }
      },
      "imports": "dnxcore50"
    }
  }
}

ここまでの準備を終えた段階で dotnet restore を実行します。

dotnet restore

f:id:daigo-knowlbo:20161108013936p:plain いい感じに、必要な依存関係モジュールを引っ張ってくれているはずです。

Migration準備

モデルクラスをSQL Serverデータベース テーブルに反映させるための コード を自動生成します。
以下のコマンドを実行します。

dotnet ef migrations add db1.0

efコマンドは、 project.json において「Microsoft.EntityFrameworkCore.Tools.DotNet」をtoolsとして指定した上で、dotnet restore を実行した為に利用可能となったオプションコマンドです。
「db1.0」は任意の名称をつける事ができます。今後、モデルクラスの追加 や 既存モデルクラスへのプロパティ追加 が発生した際に、変更セット名称的な意味合いの名称として変更していきます。

f:id:daigo-knowlbo:20161108014525p:plain

コマンド実行後 Migrations という名称のディレクトリが作成されます。この段階では、まだデータベースへの定義の反映は行われていません。

Migrationsディレクトリ配下の ls -l 結果は以下の通りです。
csコードが出力され、その中身はモデルクラスに該当するデータベーステーブルをCREATEする内容となっています。

f:id:daigo-knowlbo:20161108015134p:plain

データベースへテーブル定義(モデルクラス定義)を反映させる

ではデータベースに対して、物理的にモデルクラス定義反映させます。つまり、モデルクラスに該当する テーブル定義 を追加します。
以下のコマンドを実行します。

dotnet ef database update

f:id:daigo-knowlbo:20161108015647p:plain

データベーステーブルの確認

モデルクラス定義に従って、データベーステーブルが作成されていることを確認します。
以下がその結果であり、Company / Employee の各テーブル及びカラム定義がモデルクラスに対応する形で作成されていることを確認することができます。
また、_EFMigrationsHistoryテーブルはコードファーストでモデルクラスから物理テーブルへのリバース(マイグレーション)が行われた履歴情報を保存しています。

f:id:daigo-knowlbo:20161108015904p:plain

モデルクラスのアップデート・・・そしてテーブル定義もアップデート

開発初期バージョンは上記までの実装で無事行われたと想定します。
しかし、途中でモデルにプロパティの追加の必要性が発生したことを想定してみましょう。
影響範囲は「モデルクラス定義の更新(プロパティ追加)」及び「モデルクラスに対応するデータベーステーブルの更新(カラム追加)」となります。
まずモデルクラスの変更は、以下のようにEmployeeクラスに生年月日プロパティが追加になったと想定します。

// Employee.cs
using System;

namespace EfCoreExample.Models
{
    public class Employee 
    {
        public int id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public DateTime Birth { get; set; }  // 追加!!!!
    }
}

モデルクラスの修正が完了したら、再び以下の dotnet ef コマンドを実行します。

dotnet ef migrations add db1.1
dotnet ef database update

add以下のパラメータを「db1.1」としました。これは先程の「db1.0」からのバージョンアップによるmigrationである事を示す名称をつけました。この名称は、プロジェクトにおいて一意な名称をつける必要があります(つまり、ここで再び db1.0 と指定するとエラーとなります)。

f:id:daigo-knowlbo:20161108020910p:plain

データベース上のテーブル定義を確認すると、以下のように EmployeeテーブルにBirthカラムが追加された事を確認する事ができます。

また、__EFMigrationsHistoryテーブルの内容を確認すると、db1.0 → db1.1 と2段階のMigrationが行われた事を確認する事ができます。

f:id:daigo-knowlbo:20161108023518p:plain

では、またー。

MS公式Dockerイメージを使って.NET Core開発を行う(Mac)

.NET Coreのいくつかの開発環境・ランタイム環境のDockerイメージ(Dockerfile)はMicrosoft公式としてDocker Hubで公開されています。

microsoft/dotnet - Docker Hub

ターゲットOSは Linux のものと Windows Server 2016 Nano Server がありますが、ここでは Mac(OSx)をホストとして Linux サーバー .NET Core開発環境を作成したいと思います。

では、上記URL内の「1.0.0-preview2.1-sdk」を利用したいと思います。

下準備

Macのターミナルを開き「/Users/daigo/docker」ディレクトリをベースに進めていきたいと思います。
また、Dockerコンテナには、プログラムソースの作成・保存場所として、ホスト側(Mac)の「/Users/daigo/docker/projects/」ディレクトリを共有する事とします。

Dockerfileを作成

「1.0.0-preview2.1-sdk」の対象URL(GitHubへのリンク)にアクセスします(下図)。

f:id:daigo-knowlbo:20161107002646p:plain

これは Dockerfile となっています(https://github.com/dotnet/dotnet-docker/blob/master/1.0.0-preview2.1/debian/sdk/Dockerfile)。
↑(2017.2.16追記)上記リンクは死んでるみたい・・・適当な別バージョンで実施してくださいm( )m

f:id:daigo-knowlbo:20161107003125p:plain

これをそのまま「/Users/daigo/docker/Dockerfile」として保存します。

Dockerイメージのビルド

ターミナルで以下のコマンドを実行します。先程作成したDockerfileを元にして、Dockerイメージをビルドします(ここではイメージ名を dotnet100pv21 としています)。

docker build -t dotnet100pv21 .

イメージのダウンロードとビルドに数分程度かかると思います。
処理が完了したら以下のコマンドを実行してみましょう。dotnet100pv21という名称のイメージが作成されていることが確認できます。

docker images

f:id:daigo-knowlbo:20161107003648p:plain

Dockerコンテナの起動

ではコンテナを起動します。
ターミナルで以下のコマンドを実行します。

docker run -i -t -v /Users/daigo/Docker/Projects:/Home/Projects -p 5000:5000  dotnet100pv21

主要なコマンドオプションの意味は以下の通りです。

「-v」オプション:”ホスト側(Mac)の /Users/daigo/Docker/Projectsディレクトリ を コンテナ側の /Home/Projectsディレクトリ として公開(マッピング)”します。

「-p」オプション:”ホスト側(Mac)の 5000番ポート を コンテナ側の 5000番ポート にマッピング”します。

正常に実行されると、以下のようにコンテナ側のターミナルが表示されます。
f:id:daigo-knowlbo:20161107005605p:plain

.NET Core Webアプリ(雛形)を作成

コンテナ側ターミナル上で引き続き.NET Core Webアプリを作成したいと思います。
事前にホスト側ディレクトリにマッピングした /Home/Projects にプロジェクトを作成したいと思います。
/Home/Projectsディレクトリに移動し、まずは dotnet –info コマンドで.NET Coreの状態を確認します。

cd /Home/Projects
dotnet --info

f:id:daigo-knowlbo:20161107010020p:plain Debian上で1.0.0-preview2-1-003155の.NET Coreが動作している事が確認できます。

続けて以下のコマンドでWebアプリの雛形を作成します(新規プロジェクト作成。プロジェクトタイプはWebアプリで、という意味)。

dotnet new -t Web

f:id:daigo-knowlbo:20161107010238p:plain

ホスト側からアクセス可能にする為”微調整”

これからコンテナ側でビルド&実行するWebアプリケーションを、ホスト側(つまり外部マシン)からアクセス可能とする修正を行います。
dotnet new -t Web で自動生成されたソース中の「Program.cs」ファイルに「.UseUrls(“http://0.0.0.0:5000”)」の1行を追記します。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;

namespace WebApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup<Startup>()
                .UseUrls("http://0.0.0.0:5000")  // 追記!!!
                .Build();

            host.Run();
        }
    }
}

dotnet restore / run

dotnet restore と dotnet run コマンドを実行します。

dotnet restore
dotnet run

restoreは、このアプリケーションに必要な依存関係ファイルを、プロジェクト内にリストアします。
runは、このアプリケーションをビルド&実行します(ビルドだけ行う場合は dotnet build とします)。

ホスト側(Mac)からコンテナ側の.NET Core Webアプリにアクセス

ではホスト側であるMacSafariを起動し「http://localhost:5000」にアクセスしてみましょう。

f:id:daigo-knowlbo:20161107011709p:plain

動いた!

アクセスするURLをlocalhostとしていますが、Dockerでローカルの5000番ポートをコンテナの5000番ポートにマップしている為、これはコンテナで動作しているWebアプリケーションの実行結果となります。

見事にプロジェクトソースはホスト側ファイルシステムに保存し、開発&実行環境はポイポイ使い捨ても可能なDockerコンテナに持っていくことが出来ました!

※「本格開発」となるとソース管理でGitかましてとかなるけど、本投稿はそこまで追求しないです。

※さあ、連休中に連続投稿したので、平日はブログ更新お休みかな・・・

.NET Coreのバージョンを戻したい!(Mac)

.NET Core界隈は賑やかにバージョンアップを繰り返し、2016/10/25には 1.1 Preview 1 が登場していますね。

blogs.msdn.microsoft.com

.NET Coreマニアたちは最新バージョンに飛びついて色々試していることでしょう。
しかし、新技術でありPreview状態であったりと色々仕様変更が生じ、関連ライブラリとの整合性が合わなくなっていたりもすると思います。
Project.json廃止からの.csproj化等々、ドラスティックな変更もありますし。

いざ最新版を入れたは良いが「以前のバージョンに戻したい」という欲求にかられる事もあると思います(自分の事(汗))。
私もアンインストールしようと思ったら「あれ!?どうやって戻すの?(ダウングレードしたいよお)」となりまして・・・試行錯誤の結果、これで良いんじゃね?って方法があったので、ここで紹介&間違ってたら誰か正しき道を教えて!という事で・・・

現在の環境は・・・

まず現在の私の環境(Mac)の.NET Coreバージョンは・・・

dotnet --info

f:id:daigo-knowlbo:20161106125844p:plain

うん 1.0.0-preview3-004056 です。
ちなみにこのバージョンで dotnet new しますと、Project.jsonではなくxxx.csprojファイルが作成されます。

f:id:daigo-knowlbo:20161106130045p:plain

.NET Core本体はいずこ・・・

という事で、.NET Core本体はどこに入っているんだろう、と調べると・・・以下にありました。

/usr/local/share/dotnet/sdk

lsすると「1.0.0-preview1-002702 / 1.0.0-preview2-003121 / 1.0.0-preview2-003148 / 1.0.0-preview2.1-003155 / 1.0.0-preview3-004056」と、なんか大漁状態。 f:id:daigo-knowlbo:20161106130437p:plain

バージョン戻したいなーー

私は 1.0.0-preview2-003148 に戻したい! (これは2016/11/6現在、.NET - Powerful Open Source Cross Platform Developmentで提供されているバージョン)
(ちなみに 1.0.0-preview2.1-003155 は.NET Core 1.1のSDKってやつですよね)
新しいの使わないから、無効にしたいバージョンのフォルダを無効な名前にリネーム!・・・するとバージョン戻ります。

sudo mv 1.0.0-preview3-004056 _1.0.0-preview3-004056
sudo mv 1.0.0-preview2.1-003155 _1.0.0-preview2.1-003155

改めて dotnet --info を行なった結果が以下です。

f:id:daigo-knowlbo:20161106132440p:plain

1.0.0-preview2-003148 です!
本当にpreview2に戻ったのかなあ・・・という事で dotnet new してみましょう。

f:id:daigo-knowlbo:20161106132621p:plain

xx.csprojではなくProject.json が作成されました。中身は・・・

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {},
  "frameworks": {
    "netcoreapp1.0": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.0.1"
        }
      },
      "imports": "dnxcore50"
    }
  }
}

いつものProject.jsonです。
戻りました!(^◯^)
先ほど変更したフォルダ名を元に戻せば、元どおりになります。

※そのやり方だとだめなんだよ!ということがあればコメント等でご指摘お願い致します!

そもそもDockerイメージ使っておけば良いのかな

そもそも、物理マシンではなく・・・Microsoftが各種Dockerイメージを公開してくれているので、そっちを使っておけばガンガン使い捨てで環境戻せるかな、とも思いました。

https://hub.docker.com/r/microsoft/dotnet/

Visual Studio Code 〜 vscode-mssql拡張を使ってSQL Database(Azure)に接続する

Visual Studio Code on MacとXamarin Studioにはまっている今日この頃・・・

Visual Studio Code拡張の vscode-mssql(Microsoft SQL Server support in VS Code) を使ってAzure SQL Databaseに接続&クエリー実行するという事を試してみました。

Azure SQL Databaseの準備

まず、Azure SQL Databaseを作成します。 Azure Portalにログインし、左のメニューから「SQL データベース」を選択します。

f:id:daigo-knowlbo:20161104235358p:plain

左上の「追加」ボタンをクリックします。

f:id:daigo-knowlbo:20161105000227p:plain

データベース名・サーバー名共に「daigoExampleDb」とします。リージョンはとりあえず西日本で。

f:id:daigo-knowlbo:20161105000635p:plain

価格レベルは「S0 Standard」としました。

f:id:daigo-knowlbo:20161105000648p:plain

以上で空っぽのAzure SQL Databaseが構築できました。

ファイアウォール設定を行う

Azure SQL Databaseは初期状態ではファイアウォール設定により1433ポートが閉じられています。
クライアントPCから1433ポートでAzure SQL Databaseに接続できるようにファイアウォールの設定を変更します。
Azure Portal上で、「SQLデータベース→daigoExampleDb→概要→サーバーファイアウォールの設定」を選択し、接続を許可するIPアドレスを登録します。現在のクライアントIPアドレスは画面上に表示されるので、そのIPを開始IP・終了IPに設定すればOKです。

f:id:daigo-knowlbo:20161105002713p:plain

Microsoft SQL Server support in VS Code(vscode-mssql)拡張をインストール

Visual Studio Codeに機能拡張をインストールします。
VS Codeを起動し、左側のアイコンから一番下の「拡張機能追加」をクリックし、検索ボックスに「vscode-mssql」と入力します。
vscode-mssqlが表示されるので「インストール」をクリックします。
インストールが完了したら「Reload」ボタンをクリックして拡張機能をアクティブ化します。

f:id:daigo-knowlbo:20161105001413p:plain

接続情報の設定

Visual Studio Codeに対してAzure SQL Databaseへの接続設定を行います。
メニュー「Code→基本設定→ユーザー設定」を選択します。

f:id:daigo-knowlbo:20161105003448p:plain

ユーザー設定画面が表示されます。

f:id:daigo-knowlbo:20161105003546p:plain

ソース編集領域の右側に表示されたsettings.jsonに接続情報を追記します。
接続情報は、基本的に自分で設定した内容であり、Azure Portalで「SQLデータベース→daigoExampleDb→概要→データベース接続文字列の表示」でも確認することが出来ます。

// 既定の設定を上書きするには、このファイル内に設定を挿入します
{
    "vscode-mssql.connections": [
        {
            "server": "daigoExampledb.database.windows.net",
            "database": "daigoExampledb",
            "user": "daigo",
            "password": "xxxxxxx",
            "options":
            {
                "encrypt": true,
                "appName": "vscode-mssql"
            }
        }
    ]
}

f:id:daigo-knowlbo:20161105003854p:plain

Azure SQL Databaseに接続する

コマンドパレットを表示し、以下のコマンドを実行します。

>MSSQL:Connect to a database

f:id:daigo-knowlbo:20161105004304p:plain

SQLを実行する

メニュー「ファイル→新規作成」を選択します。
右下の言語モードの選択「プレーンテキスト」をクリックし、「SQL」に変更します。

f:id:daigo-knowlbo:20161105004520p:plain ↓↓↓↓↓↓↓ f:id:daigo-knowlbo:20161105004905p:plain

以下のCREATE TABLE / INSERT / SELECT文をエディタに記述します。(サンプルとして実行するためにテーブル作成・レコード挿入・クエリーをひとまとめにしています)

CREATE TABLE [dbo].[company](
    [id] [int] NOT NULL,
    [name] [nchar](128) NULL,
    [capital] [int] NULL
) ON [PRIMARY]

CREATE TABLE [dbo].[employee](
    [id] [int] NOT NULL,
    [company_id] [int] NULL,
    [first_name] [nvarchar](32) NULL,
    [last_name] [nvarchar](32) NULL
) ON [PRIMARY]

insert into company (id, name, capital) values(1, 'Knowlbo', 1600);
insert into company (id, name, capital) values(2, 'ClearBox Technology', 1000);
insert into company (id, name, capital) values(3, 'HogeHoge Corp', 200);

insert into employee (id, company_id, first_name, last_name) values(1, 1, 'ryuichi', 'daigo');
insert into employee (id, company_id, first_name, last_name) values(2, 1, 'hogeko', 'ito');
insert into employee (id, company_id, first_name, last_name) values(3, 2, 'sora', 'daigo');
insert into employee (id, company_id, first_name, last_name) values(4, 2, 'torao', 'majima');
insert into employee (id, company_id, first_name, last_name) values(5, 3, 'hanako', 'sasazuka');

select * from [dbo].[company];
select * from [dbo].[employee];

f:id:daigo-knowlbo:20161105005414p:plain

コマンドパレットから以下のコマンドを実行します。

>MSSQL:Run T-SQL query

テーブル作成・レコード挿入が行われ、2つのSELECTクエリーの結果が表示されます。

f:id:daigo-knowlbo:20161105005512p:plain

f:id:daigo-knowlbo:20161105005531p:plain

Visual Studio Codeは多くの機能拡張が提供されており、マルチプロジェクトのソリューションも構成でき、またGITソース管理やタスクランナーもサポートされています。
無償の開発環境ですが、かなり活用のしがいがあるソフトウェアだと思っています。

Microsoft Tech Summitに行ってきた(PartsUnlimitedをMacで動かす、のおさらい)

2016/11/1-2に開催されたMicrosoft Tech Summit 2016にちょこっとだけ参加してきました。 2日間フル参戦できれば幸せですが、打ち合わせやら作業やらの為、11/2に3セッションのみの参加となりました。

で、私が受講したのは以下の3セッション。

  • SPL003 【DevOps Global Consortium 発足記念】日本生まれの DevOps! DevOps スペシャリスト達のあくなき挑戦
  • SNR001 [ランチ付き] Azure IaaS 応用編~実務で使えるVMとPaaSの組み合わせ~
  • APP001 .NET Core アプリケーションに見るクラウド時代の開発運用サイクル (前編)

それぞれ感想はありますが、「APP001 .NET Core アプリケーションに見るクラウド時代の開発運用サイクル (前編)」で紹介されていた PartsUnlimited アプリケーションをMacでビルド&実行するというのを自分でおさらいしておこうと思います。

PartsUnlimitedを動かす!

PartsUnlimited(https://github.com/Microsoft/PartsUnlimited)とはMicrosoftが作成してGithubで公開している「DevOpsシナリオを想定した」コマースサイトの実装です。で、.NET Coreでビルド&実行することができます。

井上 章さん・小塚 大介さんのセッション中でもgithubから取得して実行するというデモがありましたが、改めて自分のMacで動作させる操作をここに記したいと思います。(なぜなら公式サイトのGettingStarted for Linux/OS Xの記述はdnuコマンドを使うというβ版だったかα版だったかの頃の.NET Coreコマンドで説明されているので。そのうち更新されるかな・・・)

私の環境はこんな感じ

まず、私のMacのNode / npmのバージョンは以下の通り。 f:id:daigo-knowlbo:20161103135856p:plain

1. PartsUnlimitedのソースを取得

gitコマンドでソースを自分のPCにcloneしましょう。 ソースを置きたい任意のフォルダに移動して、ターミナルから以下のコマンドを実行します。

git clone https://github.com/Microsoft/PartsUnlimited.git 

※ここでは「/Users/daigo/projects」ディレクトリにcloneしました。 f:id:daigo-knowlbo:20161103141029p:plain

2. bowerとgruntをインストールする

PartsUnlimitedアプリケーションでは、パッケージ管理のBowerとタスクランナーのGruntが使われています。
ですのでこれらをインストールしておく必要があります。

npmを使ってBowerとGruntをインストールします。以下のように-gを付けてグローバルにインストールしちゃってOKです。

sudo npm install bower -g 

f:id:daigo-knowlbo:20161103141516p:plain

sudo npm install grunt -g

f:id:daigo-knowlbo:20161103141758p:plain

※npm関連でエラーが出たら以下のあたりのチェックを行うと良いと思います。(まあ、発生したエラーでググれば良い感じかな。情報は多いので、なんとなく(苦労しながら?)解決することができるのではないかと・・・)

qiita.com

parashuto.com

3. dotnetコマンドでプロジェクト依存パッケージ等の取得を行う

githubからクローンしたPartsunlimitedディレクトリ配下のsrc/Partsunlimited.Modelsに移動して以下のdotnetコマンドを実行します。

dotnet restore 

f:id:daigo-knowlbo:20161103142443p:plain

次にPartsunlimitedディレクトリ配下のsrc/PartsunlimitedWebSiteに移動して以下のdotnetコマンドを実行します。

dotnet restore 

f:id:daigo-knowlbo:20161103142702p:plain

4. dotnetコマンドでプロジェクトを実行する

引き続きsrc/PartsunlimitedWebSiteディレクトリにおいて以下のdotnetコマンドを実行します。

dotnet run 

f:id:daigo-knowlbo:20161103143221p:plain 上記画像の続き↓↓↓ f:id:daigo-knowlbo:20161103143314p:plain コンパイルが成功して、localhostの5000番ポートでリスニングしたよー、と表示されます。

ブラウザで http://localhost:5000 にアクセスしたのが以下の画面です。見事にASP.NET Core / C# コードがMacで実行できました! f:id:daigo-knowlbo:20161103143544p:plain