DocumentDBのgeospatial機能で地理的検索(位置情報検索)を行う

結構以前からあったものだと思うのですが、Azure DocumentDBの「geospatial(地理空間検索)」機能を使ってみたいと思います。
緯度・経度情報をDocumentDBに保存しておき、以下のような操作を行うことが可能です。

  1. 2点間の距離の算出
  2. 1つの地理的領域が、別のもう1つの領域内に存在するかのチェック
  3. 2つの地理的領域の重なりのチェック

ここでは「地理的領域」という言葉を使用しましたが、DocumentDBでは緯度・経度情報を「点」「ライン」「ポリゴン(領域)」で保持することができます。
これらの緯度・経度情報に対してクエリーをかけることができます。

こんなアプリを作る

本投稿では、上記1番目の「2点間の距離の算出」を利用して以下のようなWebアプリを作成します。

  • 災害時避難施設情報をDocumentDBに保存しておく
  • Webアプリを作成。JavaScriptで現在位置情報を自動取得 → 近隣の避難施設を検索する
  • 避難施設の場所はGooogle Mapsへのリンクを配置することで表示する

画面キャプチャは以下の通りです。

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

「避難施設を検索」をクリックすると・・・

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

「地図を表示」をクリックすると・・・Google Mapsが別ウィンドウ表示される。

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

ソースは以下の github に上げてあります。

github.com

実装の構成は「1ソリューション・3プロジェクト構成」です。
プロジェクト構成は以下の通り。

・CreateEvacuationFacilityDb - DocumentDBデータを作成するコンソールアプリケーションプロジェクト
・EvacuationFacilityLib - DocumentDB操作を行う為のリポジトリクラスとモデルクラスを含むライブラリプロジェクト
・EvacuationFacilityWeb - 災害時避難施設を検索するWebアプリケーションプロジェクト

では以下に、ソースの解説を含めて作成手順を記述いたします。

1. DocumentDBデータを準備する

まず、データソースとなるDocumentDBデータを作成します。

DocumentDB Emulatorを起動

Azure上にDocumentDBを作成してもよいのですが、今回はローカルでお手軽にテスト可能な DocumentDB Emulator を利用します。
DocumentDB Emulatorを起動しておきましょう。タスクトレイにブルーのアイコンが現れますね。

災害時避難施設情報XMLを取得する

災害時避難施設情報は 国土数値情報ダウンロードサービス国土数値情報 避難施設データの詳細 からダウンロードすることができるのでこれを利用します。
平成24年のデータで古い気もしますが、全国の災害時避難施設の名称・住所・緯度経度情報 等々 が得られます。
今回は、データが大きいので、東京 のデータだけを取得しました。
ダウンロードファイルを解凍すると以下のようなイメージです。

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

この中から「P20-12_13.xml」を利用します。

避難施設情報XMLからDocumentDB保存用データ型に変換

XMLデータをDocumentoDBデータ保存用の型に変換する処理を用意します。
ダウンロードした P20-12_13.xml は以下のようなスキーマです。

// P20-12_13.xml
<?xml version="1.0" encoding="utf-8"?>
<ksj:Dataset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ksj="http://nlftp.mlit.go.jp/ksj/schemas/ksj-app" gml:id="P20Dataset" xsi:schemaLocation="http://nlftp.mlit.go.jp/ksj/schemas/ksj-app KsjAppSchema-P20-v1_0.xsd">
  <gml:description>国土数値情報 避難施設 インスタンス文書</gml:description>
  <gml:boundedBy>
    <gml:EnvelopeWithTimePeriod srsName="JGD2000 / (B, L)" frame="GC / JST">
      <gml:lowerCorner>20.0 123.0</gml:lowerCorner>
      <gml:upperCorner>46.0 154.0</gml:upperCorner>
      <gml:beginPosition calendarEraName="西暦">1900</gml:beginPosition>
      <gml:endPosition indeterminatePosition="unknown"/>
    </gml:EnvelopeWithTimePeriod>
  </gml:boundedBy>
  <!--図形-->
  <gml:Point gml:id="pt100001">
    <gml:pos>35.686898 139.739538</gml:pos>
  </gml:Point>
  <gml:Point gml:id="pt100002">
    <gml:pos>35.697347 139.760433</gml:pos>
  </gml:Point>
  ... 省略 ...
  <ksj:EvacuationFacilities gml:id="P20_100001">
    <ksj:position xlink:href="#pt100001"/>
    <ksj:administrativeAreaCode codeSpace="AdministrativeAreaCode.xml">13101</ksj:administrativeAreaCode>
    <ksj:name>いきいきプラザ一番町</ksj:name>
    <ksj:address>東京都千代田区一番町12-2</ksj:address>
    <ksj:facilityType>指定避難所、二次避難所</ksj:facilityType>
    <ksj:seatingCapacity>-1</ksj:seatingCapacity>
    <ksj:facilityScale>-1</ksj:facilityScale>
    <ksj:hazardClassification>
      <ksj:Classification>
        <ksj:earthquakeHazard>false</ksj:earthquakeHazard>
        <ksj:tsunamiHazard>false</ksj:tsunamiHazard>
        <ksj:windAndFloodDamage>false</ksj:windAndFloodDamage>
        <ksj:volcanicHazard>false</ksj:volcanicHazard>
        <ksj:other>false</ksj:other>
        <ksj:notSpecified>true</ksj:notSpecified>
      </ksj:Classification>
    </ksj:hazardClassification>
  </ksj:EvacuationFacilities>
  <ksj:EvacuationFacilities gml:id="P20_100002">
    <ksj:position xlink:href="#pt100002"/>
    <ksj:administrativeAreaCode codeSpace="AdministrativeAreaCode.xml">13101</ksj:administrativeAreaCode>
    <ksj:name>お茶の水小学校</ksj:name>
    <ksj:address>東京都千代田区猿楽町1-1-1</ksj:address>
    <ksj:facilityType>指定避難所、地区救援センター</ksj:facilityType>
    <ksj:seatingCapacity>-1</ksj:seatingCapacity>
    <ksj:facilityScale>-1</ksj:facilityScale>
    <ksj:hazardClassification>
      <ksj:Classification>
        <ksj:earthquakeHazard>false</ksj:earthquakeHazard>
        <ksj:tsunamiHazard>false</ksj:tsunamiHazard>
        <ksj:windAndFloodDamage>false</ksj:windAndFloodDamage>
        <ksj:volcanicHazard>false</ksj:volcanicHazard>
        <ksj:other>false</ksj:other>
        <ksj:notSpecified>true</ksj:notSpecified>
      </ksj:Classification>
    </ksj:hazardClassification>
  </ksj:EvacuationFacilities>
  ... 省略 ...
</ksj:Dataset>

上記XMLデータを元に、以下のようなJSONスキーマデータに変換してDocumentDBに保存したいと思います。

{
  "ID": "pt100001",
  "Latitude": 35.686898,
  "Longitude": 139.739538,
  "Location": {
    "type": "Point",
    "coordinates": [
      139.739538,
      35.686898
    ]
  },
  "Name": "いきいきプラザ一番町",
  "Address": "東京都千代田区一番町12-2",
  "FacilityType": "指定避難所、二次避難所"
}

ということで、上記JSONスキーマに該当するモデルクラスとして、以下の EvacuationFacilityInfo クラスを用意します。
「public Point Location」プロパティがポイントで、Point型は「Microsoft.Azure.Documents.Spatial名前空間の型」になります。
この値が地理的データクエリーの対象となります。

// EvacuationFacilityLib\Models\EvacuationFacilityInfo.cs
using Microsoft.Azure.Documents.Spatial;

namespace EvacuationFacilityLib.Models
{
  public class EvacuationFacilityInfo
  {
    // ID
    public string ID { get; set; }
    // 緯度
    public double Latitude { get; set; }
    // 経度
    public double Longitude { get; set; }
    // DocumentDBクエリー用のPoint型
    public Point Location { get; set; }
    // 施設名
    public string Name { get; set; }
    // 住所
    public string Address { get; set; }
    // 施設タイプ
    public string FacilityType { get; set; }
  }
}

XMLデータを EvacuationFacilityInfo オブジェクト配列にコンバートするロジックを実装します(GmlToCustomFormatConvertorクラス)。
実装はごちゃごちゃしていますが、「XPathでクエリーしてモデルクラスに一生懸命変換する」というごり押し実装なので説明は省きます。

// CreateEvacuationFacilityDb\GmlToCustomFormatConvertor.cs
using System.Collections.Generic;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Linq;
using Microsoft.Azure.Documents.Spatial;
using EvacuationFacilityLib.Models;

namespace CreateEvacuationFacilityDb
{
  public class GmlToCustomFormatConvertor
  {
    public static List<EvacuationFacilityInfo> Load(string xmlFilePath)
    {
      var result = new List<EvacuationFacilityInfo>();

      var xDoc = XDocument.Load(xmlFilePath);

      var nsmgr = new XmlNamespaceManager(new NameTable());
      nsmgr.AddNamespace("gml", "http://www.opengis.net/gml/3.2");
      nsmgr.AddNamespace("ksj", "http://nlftp.mlit.go.jp/ksj/schemas/ksj-app");
      nsmgr.AddNamespace("xlink", "http://www.w3.org/1999/xlink"); 

      var points = xDoc.XPathSelectElements("//gml:Point", nsmgr);
      foreach (var point in points)
      {
        var evacuationFacilityInfo = new EvacuationFacilityInfo();

        //var id = point.Attribute("gml:id").Value;
        evacuationFacilityInfo.ID = point.FirstAttribute.Value;
        
        //var pos = point.XPathSelectElement("gml:pos");
        string latitudeAndLongitude = point.Value;
        double dLatitude = double.Parse(latitudeAndLongitude.Split(' ')[0]);
        double dLongitude = double.Parse(latitudeAndLongitude.Split(' ')[1]);
        evacuationFacilityInfo.Latitude = dLatitude;
        evacuationFacilityInfo.Longitude = dLongitude;
        evacuationFacilityInfo.Location = new Point(dLongitude, dLatitude);

        var evacuationFacility = 
          xDoc.XPathSelectElement("//ksj:EvacuationFacilities/ksj:position[@xlink:href='#" + evacuationFacilityInfo.ID + "']", nsmgr).Parent;
        evacuationFacilityInfo.Name = 
          evacuationFacility.XPathSelectElement("ksj:name", nsmgr).Value;
        evacuationFacilityInfo.Address = 
          evacuationFacility.XPathSelectElement("ksj:address", nsmgr).Value;
        evacuationFacilityInfo.FacilityType = 
          evacuationFacility.XPathSelectElement("ksj:facilityType", nsmgr).Value;

        result.Add(evacuationFacilityInfo);
      }

      return result;
    }
  }
}

EvacuationFacilityInfoオブジェクトをDocumentDBに保存

XMLをコンバートして得られたEvacuationFacilityInfoオブジェクトをDocumentDBに保存するリポジトリークラスも作成しましょう。
データベースの作成・コレクションの作成・ドキュメントの保存・ドキュメントの地理的検索まで実装してしまいます。

// EvacuationFacilityInfoRepository.cs
#define LOCAL_DB

using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;

using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;

using Microsoft.Azure.Documents.Spatial;

using EvacuationFacilityLib.Models;

namespace EvacuationFacilityLib.Repositories
{
  public class EvacuationFacilityInfoRepository
  {
    /// <summary>
    /// 任意のデータベースID
    /// </summary>
    private static readonly string DatabaseId = "EvacuationFacilityInfoDb";

    /// <summary>
    /// 任意のコレクションID
    /// </summary>
    private static readonly string CollectionId = "EvacuationFacilityInfoCollection";

    /// <summary>
    /// エンドポイント
    /// </summary>
#if LOCAL_DB
    private static readonly string EndPoint = "https://localhost:8081/";
#else
    private static readonly string EndPoint = "[your endpoint]";
#endif

    /// <summary>
    /// 認証キー(固定)
    /// </summary>
#if LOCAL_DB
      private static readonly string AuthKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
#else
    private static readonly string AuthKey = "[your auth key]";
#endif

    private static DocumentClient client;


    /// <summary>
    /// EvacuationFacilityInfoオブジェクトを保存します。
    /// </summary>
    public static void SaveEvacuationFacilityInfos(List<EvacuationFacilityInfo> evacuationFacilityInfos)
    {
      foreach (var info in evacuationFacilityInfos)
      {
        client.UpsertDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), info);
      }
    }

    /// <summary>
    /// 緯度経度に対する距離範囲に存在するデータ(避難施設)を取得します。
    /// </summary>
    public static List<EvacuationFacilityInfo> Search(double longitude, double latitude, int distance)
    {
      List<EvacuationFacilityInfo> result = null;

      var query = 
        client.CreateDocumentQuery<EvacuationFacilityInfo>(
          UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
          new FeedOptions { MaxItemCount = -1 })
          .Where(i => i.Location.Distance(new Point(longitude, latitude)) < distance);

      result = query.ToList();

      return result;
    }

    /// <summary>
    /// データベース・コレクションの初期化を行います。
    /// </summary>
    public static void Initialize(bool initDatabase = false)
    {
      client = new DocumentClient(new Uri(EndPoint), AuthKey, new ConnectionPolicy { EnableEndpointDiscovery = false });
      if (initDatabase)
      {
        CreateDatabaseIfNotExistsAsync().Wait();
        CreateCollectionIfNotExistsAsync().Wait();
      }
    }

    /// <summary>
    /// 存在しなければデータベースを作成します。
    /// </summary>
    /// <returns></returns>
    private static async Task CreateDatabaseIfNotExistsAsync()
    {
      try
      {
        await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(DatabaseId));
      }
      catch (DocumentClientException e)
      {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
          await client.CreateDatabaseAsync(new Database { Id = DatabaseId });
        }
        else
        {
          throw;
        }
      }
      catch
      {
        throw;
      }
    }

    /// <summary>
    /// 存在しなければコレクションを作成します。
    /// </summary>
    /// <returns></returns>
    private static async Task CreateCollectionIfNotExistsAsync()
    {
      try
      {
        await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId));
      }
      catch (DocumentClientException e)
      {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
          // DataType.Point に対して SpatialIndex 付加してコレクションを作成
          await client.CreateDocumentCollectionAsync(
            UriFactory.CreateDatabaseUri(DatabaseId),
            new DocumentCollection { Id = CollectionId, IndexingPolicy = new IndexingPolicy(new SpatialIndex(DataType.Point)) },
            new RequestOptions { OfferThroughput = 1000 });
        }
        else
        {
          throw;
        }
      }
    }
  }
}

DatabaseId / CollectionIdは任意ですが、ここではそれぞれ「EvacuationFacilityInfoDb」「EvacuationFacilityInfoCollection」としました。
EndPointはローカルのDocumentDB Emulatorを使用するため「https://localhost:8081/」となります。AuthKeyもEmulatorである為、固定値となります。
エミュレータではなくAzure DocumentDBを利用する場合には、適切なEndPointとAuthKeyを指定してください(あなたのAzureポータルから確認可能)。
Initialize()メソッドで接続用DocumentClientオブジェクトを作成します。引数initDatabaseがtrueの場合は、データベース・コレクションの作成も行います。
CreateCollectionIfNotExistsAsync()メソッドではコレクションを作成します。ここでポイントがあります。地理的クエリーを行う為に、インデックスポリシーとしてSpatialIndexを指定してます。
SaveEvacuationFacilityInfos()メソッドは、EvacuationFacilityInfoオブジェクト配列をDocumentDBに保存します。
Search()メソッドは、データの中から、引数で指定した緯度経度に対する距離範囲内の位置にある避難施設を検索します。
以下の様に距離範囲のクエリー条件を記述することができます。

.Where(i => i.Location.Distance(new Point(longitude, latitude)) < distance);

Microsoft.Azure.Documents.Spatial.Pointクラスのコンストラクタ引数は、(緯度, 経度)ではなく (経度, 緯度) なのでご注意を。

コンバート及びDocumentDB作成処理の呼び出し

上記で用意した各クラス・メソッドを呼び出して「XMLデータからEvacuationFacilityInfoオブジェクトに変換、さらにDocumentDBに保存」までを行うコンソールアプリ「CreateEvacuationFacilityDb」のMain()メソッドを実装します。

// CreateEvacuationFacilityDb\Program.cs
using System.Collections.Generic;

using EvacuationFacilityLib.Models;
using EvacuationFacilityLib.Repositories;

namespace CreateEvacuationFacilityDb
{
  class Program
  {
    static void Main(string[] args)
    {
      // DocumentDB初期化
      EvacuationFacilityInfoRepository.Initialize(true);
      
      // XMLをロードしてカスタムクラスコレクションに変換
      // xmlのパスは書き換えてください!
      List<EvacuationFacilityInfo> evacuationFacilityInfoList = 
        GmlToCustomFormatConvertor.Load(@"J:\Projects\Github\research\DocumentDB\GIS\P20-12_13.xml");

      // カスタムクラスコレクションをDocumentDBに保存
      EvacuationFacilityInfoRepository.SaveEvacuationFacilityInfos(evacuationFacilityInfoList);
    }

  }
}

読み込むXMLファイルのパスは皆さんの環境に合わせて書き換えてください。
CreateEvacuationFacilityDbコンソールアプリを実行すると、DocumentDBにデータが作成・格納されます。

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

2. Webアプリを作成する

EvacuationFacilityWebアプリを作成します。

DocumentDB接続を初期化

まずGlobal.asax.csのStartで「EvacuationFacilityInfoRepository.Initialize()」を呼び出し、DocumentClientオブジェクトを初期化します。

// EvacuationFacilityWeb\Global.asax.cs
public class MvcApplication : System.Web.HttpApplication
{
  protected void Application_Start()
  {
    // DocumentDB初期化
    EvacuationFacilityInfoRepository.Initialize();

    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
  }

コントローラークラスを実装

コントローラクラスを実装します。
検索画面表示の為のHTTP GET用Index()メソッド、「避難施設を検索」クリック時のHTTP POST用Index()メソッドを実装します。
POST時には検索結果をViewBagにしまって resultビュー を表示します。

// EvacuationFacilityWeb\Controllers\HomeController.cs
using System.Web.Mvc;

using EvacuationFacilityLib.Repositories;

namespace EvacuationFacilityWeb.Controllers
{
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      return View();
    }

    [HttpPost]
    public ActionResult Index(double latitude, double longitude, int distance)
    {
      ViewBag.EvacuationFacilities = EvacuationFacilityInfoRepository.Search(longitude, latitude, distance);

      return View("result");
    }
  }
}

ビューを実装

Index及びResultのビュー定義は以下の通りです。
実行時の緯度経度は navigator.geolocation.getCurrentPosition() で取得します。

// EvacuationFacilityWeb\Views\Home\Index.cshtml
<script type="text/javascript">
  if ("geolocation" in navigator) {
    navigator.geolocation.getCurrentPosition(function (position) {
      // POST用
      $('#latitude').val(position.coords.latitude);
      $('#longitude').val(position.coords.longitude);
      // 表示用
      $('#spanLatitude').html(position.coords.latitude);
      $('#spanLongitude').html(position.coords.longitude);

      $('#currentLocationGmapUrl').attr('href', 'https://www.google.co.jp/maps/place/' + position.coords.latitude + '+' + position.coords.longitude);
    },
    function (err) {
      alert(err.code + ': ' + err.message);
    });
  } else {
    alert('geolocation is not supported.');
}

</script>

<style>
.row {
  padding-top: 5px;
  padding-bottom: 5px;
}
</style>

@using (Html.BeginForm())
{
<div class="row">
  <div class="col-xs-2"></div>
  <div class="col-xs-8">
    近隣の避難施設を検索します。
  </div>
  <div class="col-xs-2"></div>
</div>
<div class="row">
  <div class="col-xs-5 text-right">
    現在地:
  </div>
  <div class="col-xs-7">
    緯度<span id="spanLatitude"></span> 経度:<span id="spanLongitude"></span>
    <br />
    <a id="currentLocationGmapUrl" target="_blank">地図を表示</a>
  </div>
</div>
<div class="row">
  <div class="col-xs-5 text-right">
    距離範囲:
  </div>
  <div class="col-xs-7">
      <select id="distance" name="distance">
        <option value="500">0.5 km 範囲内</option>
        <option value="1000">1 km 範囲内</option>
        <option value="3000">3 km 範囲内</option>
        <option value="5000">5 km 範囲内</option>
      </select>
  </div>
</div>
<div class="row">
  <div class="col-xs-5 text-right">

  </div>
  <div class="col-xs-7">
    <input type="hidden" id="latitude" name="latitude" />
    <input type="hidden" id="longitude" name="longitude" />
    <input type="submit" value="避難施設を検索" class="btn btn-default" />
  </div>
</div>
}
<hr />
「navigator.geolocation.getCurrentPosition()」で現在位置を取得するので、LANだとIPが降られた場所で遠くになるかも・・・

以下は resultビュー です。
検索結果はHomeControllerにより ViewBag.EvacuationFacilities にEvacuationFacilityInfo配列として格納されています。
それらのデータを一覧レンダリングします。 Google Mapsへのリンクはソースにあるように緯度・経度 をクエリーパラメータとすることで実装します。

// EvacuationFacilityWeb\Views\Home\result.cshtml
<h2>検索結果</h2>

<a href="./">戻る</a>

@if (this.ViewBag.EvacuationFacilities != null)
{
  <p>@this.ViewBag.EvacuationFacilities.Count 件</p>
  foreach (EvacuationFacilityLib.Models.EvacuationFacilityInfo facility in this.ViewBag.EvacuationFacilities)
  {
    <p>名称:@facility.Name</p>
    <p>住所:@facility.Address</p>
    <p>施設タイプ:@facility.FacilityType</p>
    <p><a target="_blank" href='@System.String.Format("https://www.google.co.jp/maps/place/{0}+{1}", facility.Latitude, facility.Longitude)'>地図を表示</a></p>
    <hr />
    
  }
}

まとめ

緯度・軽度からのクエリーというのは、なかなか色々なところで使えそうですね。
特にスマホアプリ系では利用用途が多そうに思います。スマホネイティブ(Xamarin Forms含め)なら位置情報もより正確に取得出来、マップを含めたアプリ連携がよりスムーズです。
さらに技術を進めると単純な位置関係だけでなく、道路を考慮した実移動距離となってきますが、Azureに限らずどんどん進化していくような気がしています。

ということで、今回、ソースの解説は結構 雑い かったと思いますが、一式をgithubにあげたので、そちらを見ていただければ幸いです。

Visual Studio for Mac から ASP.NET CoreアプリをAzureにパブリッシュ!

Visual Studio for MacASP.NET CoreアプリをAzure App Service にパブリッシュしてみたいと思います。

では早速。(Visual Studio for MacGUIとウィザードが自動出力するコードのみを使ったノンコーディング操作で行きます。)

1. プロジェクトの作成

Visual Studio for Macを起動し、ASP.NET Core Webアプリケーションを作成します。
メニュー「ファイル → 新しいソリューション」を選択します。
「新しいプロジェクト」ダイアログで「.NET Core → App → ASP.NET Core Web App」を選択して「次へ」をクリックします。

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

プロジェクト名・ソリューション名を入力して「作成」をクリックします。

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

カラカラとNugetパッケージの復元が行われるので、完了するまで少し待ちます・・・

2. Azureにパブリッシュ

ソリューションからプロジェクト名を右マウスクリックし「Publish → Publish to Azure」をクリックします。

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

パブリッシュ済みのAzure App Serviceの一覧がリストされます。
今回は新規に追加したいので左下の「ニュース」をクリックします(ニュース・・・というか 新規 だと思う・・・)。

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

(そして更に、また謎の日本語のダイアログが表示されます)

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

Android サービス」・・・実際にはAzure App Serviceが作成されるのでご心配なく・・・
「作成済み」ボタン・・・「作成」と認識してください。
(この辺りは2017/4/14時点なので、そのうち正しい日本語にローカライズされるでしょう・・・)
まあ、Azureではおなじみの アプリ名 や リソースグループ名 価格プラン 等を入力します。
ちなみに今回は「プロジェクト名」は CoreWebApp1 としたのですが、Azureにパブリッシュする「アプリ名」は RdCoreWebApp1 としています。この名称は異なっていてもOKです。

パブリッシュが完了すると、ブラウザで対象アプリが実行されます。

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

3. Azureポータルで確認

Azureポータルに行き、今回パブリッシュしたアプリ(リソースグループ)を確認したのが以下の画像キャプチャです。

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

4. まとめ

はい!
サクサクっとVisual Studio for MacからAzureにパブリッシュできました!
Visual Studio for Macが Preview から 正式版 になるのが待ち遠しいです。
そして各種環境の.NET Standard 2.0実装がリリースされれば、更にWindowsMacの敷居が低くなるでしょう^^

Azure FunctionsをVS2017でローカル実行&デバッグしてパブリッシュする

MSDNブログ(↓↓↓)で Azure Functions 関連の記事を見たので自分でやってみました。

blogs.msdn.microsoft.com

上記ブログに記述されている事、そして今回自分で試した事を要約すると以下の内容です。

  1. Azure FunctionsをVisual Studio 2017で開発&デバッグする(つまりローカル実行するということ)
  2. Azure Functionsをクラスライブラリとしてビルド&パブリッシュする

1 は、まあ 開発効率を上げるということですね。
Azure Functions自体はAzureポータルでコーディングできますが、本格的実装はローカルで行いたいですよね。

2 は、クラスライブラリとしてアセンブリ(dll)にしてデプロイすることで実行パフォーマンスを上げる意味を持ちます。
Azure Functionsは5分間非アクティブであるとアイドル状態となり、次の実行時には再度メモリ上に展開されます。C# / JavaScriptコードがデプロイされている場合、アセンブリへのコンパイル作業から再実行され起動パフォーマンスが落ちてしまいます。

では、以下に手順を・・・

ちなみに、Visual Studioとの統合ツール「Visual Studio Tools for Azure Functions」がまだ 2017 対応できていないらしく、そのため以下のような手動による操作がいくつか発生します。でも、あと数週間もすればツールがリリースされるみたいです。

Azure Functions CLI のインストール

Azure Functions CLI をインストールします。
以下のnpmコマンドでグローバル領域にインストールします。

npm i -g azure-functions-cli

www.npmjs.com

Visual Studio 2017 でWebプロジェクトを作成

Visual Studio 2017を起動します。
メニュー「ファイル → 新規作成 → プロジェクト」を選択します。
プロジェクトテンプレートは「Web → ASP.NET Web アプリケーション(.NET Framework)」、プロジェクト名はここでは「AzureFunctionsWeb」としました。

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

「空」テンプレートを選択します。

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

プロジェクトのプロパティを表示します(ソリューションエクスプローラからプロジェクトをマウス右ボタンクリックメニューで「プロパティ」を選択)。

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

Webタブを選択し、以下の設定を行います。
* 「外部プログラムを起動する」を選択。
* パスは「C:\Users\「【ユーザー名】\AppData\Roaming\npm\node_modules\azure-functions-cli\bin\func.exe」とする。
* 「コマンドライン引数」は「host start」とする。
* 「作業ディレクトリ」は【プロジェクトフォルダ】とする。

設定を行った画面は以下の通り。

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

Azure Portal上で Azure Functions を作成

Visual Studio Tools for Azure Functionsがリリースされれば、VSからプロジェクトを生成できると思いますが、それが今はないのでポータルで作成して、ローカルVSにベースとなるソースを落としてきます)

Azure Functions 作成画面を表示

https://functions.azure.com/signinをブラウザで開きます。
「New function app」の「Name」に作成するファンクション名を入力します。ここでは「RyuichiFunction0916」としました。
「Region」は「Japan East」としました。
「Create + get started」をクリックします。

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

「WebHook + API」「C#」を選択し、「この関数を作成する」ボタンをクリックします。

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

作成完了。

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

Azure Functions ソースのダウンロード

左下の「Function App の設定」をクリックします。

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

「Kuduに移動」をクリックします。

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

「site」を選択します。

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

「wwwroot」のダウンロードアイコンをクリックします。

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

ダウンロードしたzipのイメージは以下の通り。

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

WebプロジェクトにFunctionsソースを追加

先程作成したWebプロジェクト(AzureFunctionsWeb)フォルダにzipの内容を解凍します。

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

追加のファイルをプロジェクトに含めます。
また「run.csx」ファイルを「run.cs」にリネームします。

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

run.csを以下のように修正します。
run.csxでは単純に run() だけが定義されていますが、csコードにする為「名前空間・クラス」でくくります。
また必要な using を追加します。
レスポンス文字列も若干修正を加えました。

// run.cs
using Microsoft.Azure.WebJobs.Host;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace AzureFunctionsWeb
{
  public class ExampleFunction
  {
    public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
    {
      log.Info("C# HTTP trigger function processed a request.");

      // parse query parameter
      string name = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0)
        .Value;

      // Get request body
      dynamic data = await req.Content.ReadAsAsync<object>();

      // Set name to query string or body data
      name = name ?? data?.name;

      return name == null
        ? req.CreateResponse(HttpStatusCode.BadRequest, 
                             "Please pass a name on the query string or in the request body")
        : req.CreateResponse(HttpStatusCode.OK,
                             "Hello " + 
                              name + 
                              ". I'm assembly!!");
    }
  }
}

プロジェクトにNugetパッケージを追加

Nugetで以下のパッケージを追加します。

function.jsonの調整

function.jsonに「scriptFile」「entryPoint」キーを追加します。
scriptFileにはアセンブリ名を、entryPointには名前空間/クラス付きのRun()メソッドの完全名を記述します。

// function.json
{
  "scriptFile": "..\\bin\\AzureFunctionsWeb.dll",
  "entryPoint": "AzureFunctionsWeb.ExampleFunction.Run",
  "disabled": false,
  "bindings": [
    {
      "authLevel": "function",
      "name": "req",
      "type": "httpTrigger",
      "direction": "in"
    },
    {
      "name": "$return",
      "type": "http",
      "direction": "out"
    }
  ]
}

ローカル実行

Visual StudioでF5クリックにより実行を行います。
以下のようなコマンドプロンプトが表示され Function がリクエスト待ち(実行待ち)状態となります。

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

コマンドプロンプト表示上に、以下の記述を確認することができます。

Job host started
Http Function HttpTriggerCSharp1: http://localhost:7071/api/HttpTriggerCSharp1

このURLがFunctionの受け口のURLになります。

サンプル(ExampleFunction)は「name」というキーをパラメータとして受け取ります。その為、以下のURLをブラウザでアクセスしてみましょう。

http://localhost:7071/api/HttpTriggerCSharp1?name=daigo

Functionが実行され以下のようなレスポンスが得られます。

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

ブレークポイントもきちんと止まります。

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

Azure FunctionsへのPublish

次に、ローカルで実装を行った Function を AzureにPublishします。
コマンドプロンプトを起動します。

作成したWebプロジェクトフォルダ(F:\Projects\Research\AzureFunctionsWeb\AzureFunctionsWeb)に移動します。

以下のコマンドを実行してCLIからAzureにログインします。

func azure login  

Azureへのログイン画面が表示されるので、ログインを行います。

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

以下のコマンドで、現在PublishされているFunctionの一覧を確認します。

func azure functionapp list

現在の私の環境では2つのFunctionがPublishされていました。
RyuichiFunction0916の方が、今回作成したものです。

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

以下のコマンドによりFunctionをローカルからAzureへPublishします。
(RyuichiFunction0916部分は適時 Function 名に置き換えます)

func azure functionapp publish RyuichiFunction0916

正常にPublishされた結果画面が以下になります。

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

Azure Functionsを実行

Azureポータルで「RyuichiFunction0916」を表示。
「開発」タブを選択、「テスト」タブを選択、「要求本文」をテスト文字列に修正、「実行ボタン」クリック、を行います。
以下のように「出力」に「"Hello Daigo. I’m assembly!!“」が表示されました。
VS2017上で実装したロジックが実行されていることが確認できます。

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

まとめ

VS2017用「Visual Studio Tools for Azure Functions」がリリースされれば、上記のようなゴニョゴニョした作業はバックエンドで自動化されると思います。ただ、CLIなんかで、裏の動きを知っておくのは良いことだと思っています。
また、Azure functionsやマイクロサービスについては、私自身はまだ実運用の場面では使ったことがないので、色々な知見を経験者の方から学びたいと思っております。
WebHookを絡めてGithubだったりSlackだったりと連携する実装なんかも比較的容易に出来るようなので、色々試してみたいと思います。

Azure VM 2台 を超簡単にLoad Balancer構成してみた

先日、Azureの障害祭りがあったということで・・・というわけではないですが(笑)
2台構成の Azure Virtual Machine(高可用性)を、「超簡単に」組むというのをやってみました。

また、今更どうこう言うまでもなく ResourceManager ベースになってから、Azureの構成は、よりオートメーション化が可能になっています。

Azure QuickStart Template

GithubのAzure QuickStart Templateというところに、かなり使えるテンプレート(json構成ファイル)がたくさん落ちています。

github.com

もう山のようにいろんなテンプレートが落ちていますね。
これらをベースにjsonファイルいじって、自分の必要とする構成をカスタムに構成すれば工数の削減につながりますね。
また、ドキュメント読んだだけじゃ、よくわからなかったなぁ・・というようなものも、実際に動くテンプレートサンプルを使うと非常に理解しやすいですね。

2台構成&LoadBalancerのテンプレート

上記 Azure QuickStart Template の中から今回使うのは「2台構成&LoadBalancerのテンプレート」ということで以下になります。

github.com

上記URLを開くと、親切にも以下の赤枠にあるように「Deploy to Azure」というリンクが用意されています。

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

リンク先は以下の通りで、Githubに置いてある azuredeploy.json を Azureポータルに食わせるリンクになっています。

https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-quickstart-templates%2Fmaster%2F201-2-vms-loadbalancer-lbrules%2Fazuredeploy.json

デプロイ

「Deploy to Azure」リンクをクリックすると、Azure Portalに移動し、構成を組むためのパラメータの入力画面になります。
パラメータを適当に埋めます。

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

ここではリージョンは西日本にしました。
また、VMサイズは、このテンプレートではデフォルトで Standard_D1 になっています。
「購入」ボタンをクリックしましょう。

「リソース グループ」を確認すると、「Win2VM(先程のパラメータで入力したリソースグループ名)」が作成されています。

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

VMが作成されるまで数分かかると思うので、作成が完了するまで待ちましょう。

構成を確認する

Win2VMリソースグループは以下のような構成です。

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

以下のような、2台のVMロードバランサーする最低限の構成がしっかり組まれています。

  • 可用性セット
  • ロードバランサー
  • パブリックIP
  • Virtual Machine + Disk + NIC 2つ
  • 仮想ネットワーク
  • ストレージアカウント

myAvSet(可用性セット)

myAvSet(可用性セット)は、以下のような構成になっています。

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

障害ドメインは2つ、更新ドメインも2つ、それぞれのドメインに対してVMが割り当てられています。
AzureポータルのGUI上から可用性セットを作成すると「障害ドメイン 2つ、更新ドメイン 5つ」がデフォルトになっています。
この構成JSONをベースにして3台以上の構成をとる場合には適時 障害ドメイン及び更新ドメインを拡張する必要があります。
更新ドメインが2つのまま3台目のVMを追加すると、1つ目のVM(myVM0)と同一更新ドメインに配置されてしまいます。つまり、メンテナンス時に2つのVMが同時に再起動される可能性が発生してしまいます。

ロードバランサーのプローブ

ロードバランサーのプローブは、TCPでポート80、5秒ごとのチェック、以上閾値は2、で構成されています。

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

Virtual Machine のIISを構成

作成された Virtual MachineにはまだIISが構成されていません。
2つのVMにそれぞれIISを構成しましょう。
myVM0 を選択し、「接続」をクリックするとリモートデスクトップ接続ファイルがダウンロードされます。

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

通常のWindows Serverへのリモートデスクトップになりますので、IISを構成しましょう。

まあ、例のコレですね・・・(途中端折ります)

f:id:daigo-knowlbo:20170321015434p:plain
f:id:daigo-knowlbo:20170321015544p:plain
f:id:daigo-knowlbo:20170321015552p:plain
f:id:daigo-knowlbo:20170321015602p:plain

※myVM1にも同様にIISを構成しましょう。

LoadBalancerの動きを確認する

これでクライアントからのHTTPリクエストに対して、「2台に負荷分散&1台に障害が発生したらもう1台に振り分け」という高可用性なサーバー構成が完了しました。

本当に障害発生時にもう片方にリクエストが振り分けられるかどうか確認してみます。
myVM0 の c:\inetpub\wwwroot に、以下の index.html を配置します。

Hello. I'm myVm0.

myVM1 の c:\inetpub\wwwroot には、以下の index.html を配置します。

Hello. I'm myVm1.

URL(index.html)にアクセスるする

では、作成した index.html にアクセスしてみましょう。
ブラウザで、URLにアクセスします。
URLのIPアドレスロードバランサー(myLB)の「パブリックIPアドレス」で確認することができます。
ここでは「52.175.152.10」でした。

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

http://52.175.152.10」にブラウザでアクセスすると・・・私の環境では myVM0 につながりました。

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

では myVM0 のIISを停止してみます。

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

再び「http://52.175.152.10」をアクセスすると・・・

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

myVM1 につながりました!OKです!

まとめ

一昔前はクラスタリング構成のサーバーを組む(もしくは実証試験する)なんて、物理サーバー2台用意して、ケーブルつないで、OSメディア用意して・・・なんて大ごとでしたが、Azureのようなクラウド環境を利用するとサクサクっと社会人1年目でも簡単に組めちゃう素晴らしき時代ですね!
ただ、そのベースにある技術を理解しておくことが重要だと思います!(って人に言える程、私自身が有識者ではないが・・・)

Azure DocumentDB Emulatorを使ってみた。で、.NET Coreから操作した話。

技術者としての尊敬の対象である Scott Hanselman 氏のブログで「Azure DocumentDB Emulator」についての記事が書かれていたので、自分でも使ってみました。

www.hanselman.com

こんなことをやった

「Azure DocumentDB」は、もはや、広く知られた Azureが提供する NoSQL データベースです。
いわゆる「ちょー早くて、ちょースケーラブルで、RDBの概念は捨ててから使ってね」っていうデータベースですね。
NoSQLは、その登場以来、どこにどう使うべきか?という部分についての議論・検討が行われ、まだまだ一般化していないように思います。
私自身も実は会社の業務としてNoSQLを採用したプロジェクトに関わった事はありません・・・

まあ、それはさておき(また、本投稿はNoSQLの本質を議論する趣旨ではないので・・・)、今回は以下のことを試してみたので、それらをまとめておこうと思います。

  • 「Azure DocumentDB Emulator」をインストールしてみた
  • 「.NET Core Consoleアプリ」から「Azure DocumentDB Emulator」に接続してデータ操作してみた
  • 「Azure DocumentDB(クラウド)」から「Azure DocumentDB Emulator(ローカル)」にデータをエクスポート(リストア)してみた
  • データのコンソール出力にはSerilogを使った

「Azure DocumentDB Emulator」のインストール

さあ、今回の主役である「Azure DocumentDB Emulator」をインストールします。
この「Azure DocumentDB Emulator」は、2016/11にリリースされました。
これは クラウドのAzure DocumentDB をローカルPC上でエミュレートするソフトウェアです。オフライン状態でも、Azure DocumentDB開発が可能になります。
以下のリンクページ 上部の「Download the Emulator」をクリックするとインストーラをダウンロードする事が出来ます。
(ちなみにこれはWindows版のみでMac版は残念ながらありません)

docs.microsoft.com

ダウンロードした msi ファイルは、ダブルクリックし「Next」や「OK」や、指示通りにクリックしていればインストール完了します。
インストールが完了すると、自動的に起動し、タスクトレイに以下のようなアイコンが表示されます(スタートメニューから「DocumentDB Emulator」を選択しても明示的に起動する事が出来ます)。

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

また、このアイコンをマウス右クリックすると以下のようなメニューが表示されます。

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

「Open Data Explorer...」をクリックすると以下のような管理画面が表示されます(インストール直後は、この画面も自動で表示されるはずです)。

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

見てわかるように「https://localhost:8081/_explorer/index.html#」という8081ポートでホストされた管理コンソール画面となります。
「.NET / .NET Core / Java...」等のタブが有り、ここに表示されている「Code Samples」をクリックすると、何も考えなくても「ビルド→実行」が可能なサンプルプロジェクトをダウンロード可能です。
画面上部の「Explorer」タブをクリックすると、DocumentDB Emulatorに保存されているデータベース内容を確認する事が出来ます(以下の画面)。
初期状態では 空 になります。

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

「.NET Core Consoleアプリ」+「Azure DocumentDB Emulator」でデータ保存・読み込み

では、せっかくなので最新の Visual Studio 2017 RC + .NET Core を使って DocumentDB Emulator に接続してみます。

①プロジェクトの作成

Visual Studio 2017 RCを起動し、メニュー「ファイル→新規作成→プロジェクト」を選択します。

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

ここではシンプルに、テンプレートとして「Console App(.NET Core)」を選択し、プロジェクト名は「DocDbExampleCoreConsole」としました。

②Nuget参照の追加

次にNugetで必要なライブラリへの参照を追加します。
メニュー「プロジェクト→Nuget パッケージの管理」を選択します。
まず必要なのは「Microsoft.Azure.DocumentDB.Core」になります。

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

さらに、これは本投稿の本質ではありませんが、コンソールへのデータログ出力用に「Serilog」および、そのコンソールSinkである「Serilog.Sinks.Console」も追加します。

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

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

③モデルクラスの作成

DocumentDBに保存するモデルクラスを作成します。
ブログの投稿内容を想定したモデルを作成することとします。
メニュー「プロジェクト→クラスの追加」を選択し、表示された「新しい項目の追加」ウィンドウで「コード→クラス」を選択し、名前は「BlogPost.cs」とします。

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

実装は以下のとおりであり、ブログ投稿を表す「BlogPostクラス」、ブログ投稿者を表す「Userクラス」、ブログ投稿へのコメントを表す「Commentクラス」から構成されます。

// BlogPost.cs
using System.Collections.Generic;
using Newtonsoft.Json;

namespace DocDbExampleCoreConsole
{
  /// <summary>
  /// ブログポスト(1投稿)を表すクラスです。
  /// </summary>
  public class BlogPost
  {
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "title")]
    public string Title { get; set; }

    [JsonProperty(PropertyName = "contents")]
    public string Contents { get; set; }

    [JsonProperty(PropertyName = "author")]
    public User Author { get; set; }

    [JsonProperty(PropertyName = "comments")]
    public List<Comment> Comments { get; set; }
  }

  /// <summary>
  /// ブログ投稿者を表すクラスです。
  /// </summary>
  public class User
  {
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "name")]
    public string Name { get; set; }
  }

  /// <summary>
  /// ブログ投稿へのコメントを表すクラスです。
  /// </summary>
  public class Comment
  {
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "text")]
    public string Text { get; set; }
  }
}

リポジトリクラスの作成

モデルクラスをDocumentDBに出し入れするためのデータベースアクセス用のリポジトリクラスを作成します。
メニュー「プロジェクト→クラスの追加」を選択し、表示された「新しい項目の追加」ウィンドウで「コード→クラス」を選択し、名前は「BlogPostRepository.cs」とします。

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

実装は以下の通りです。

// BlogPostRepository.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Documents.Linq;

namespace DocDbExampleCoreConsole
{
  public class BlogPostRepository
  {
    /// <summary>
    /// 任意のデータベースID
    /// </summary>
    private static readonly string DatabaseId = "Blog";

    /// <summary>
    /// 任意のコレクションID
    /// </summary>
    private static readonly string CollectionId = "BlogPost";

    /// <summary>
    /// エンドポイント
    /// </summary>
    private static readonly string EndPoint = "https://localhost:8081/";

    /// <summary>
    /// 認証キー(固定)
    /// </summary>
    private static readonly string AuthKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";

    private static DocumentClient client;

    /// <summary>
    /// ブログポストデータを作成します。
    /// </summary>
    /// <param name="blogPost"></param>
    /// <returns></returns>
    public static async Task<Document> CreateBlobPostAsync(BlogPost blogPost)
    {
      return await client.CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), blogPost);
    }

    /// <summary>
    /// すべてのブログポストデータを取得します。
    /// </summary>
    /// <returns></returns>
    public static async Task<IEnumerable<BlogPost>> GetAllBlogPostsAsync()
    {
      IDocumentQuery<BlogPost> query = client.CreateDocumentQuery<BlogPost>(
        UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
        new FeedOptions { MaxItemCount = -1 })
        .AsDocumentQuery();

      List<BlogPost> results = new List<BlogPost>();
      while (query.HasMoreResults)
      {
        results.AddRange(await query.ExecuteNextAsync<BlogPost>());
      }

      return results;
    }

    /// <summary>
    /// データベース・コレクションの初期化を行います。
    /// </summary>
    public static void Initialize()
    {
      client = new DocumentClient(new Uri(EndPoint), AuthKey, new ConnectionPolicy { EnableEndpointDiscovery = false });
      CreateDatabaseIfNotExistsAsync().Wait();
      CreateCollectionIfNotExistsAsync().Wait();
    }
    
    /// <summary>
    /// 存在しなければデータベースを作成します。
    /// </summary>
    /// <returns></returns>
    private static async Task CreateDatabaseIfNotExistsAsync()
    {
      try
      {
        await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(DatabaseId));
      }
      catch (DocumentClientException e)
      {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
          await client.CreateDatabaseAsync(new Database { Id = DatabaseId });
        }
        else
        {
          throw;
        }
      }
    }

    /// <summary>
    /// 存在しなければコレクションを作成します。
    /// </summary>
    /// <returns></returns>
    private static async Task CreateCollectionIfNotExistsAsync()
    {
      try
      {
        await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId));
      }
      catch (DocumentClientException e)
      {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
          await client.CreateDocumentCollectionAsync(
            UriFactory.CreateDatabaseUri(DatabaseId),
            new DocumentCollection { Id = CollectionId },
            new RequestOptions { OfferThroughput = 1000 });
        }
        else
        {
          throw;
        }
      }
    }
  }
}

リポジトリクラス実装のポイントは以下の通りです。

  • DatabaseId / CollectionId
    これらは任意の名称をつけます。ここでは「Blog」「BlogPost」としました。

  • EndPoint
    エンドポイントはデフォルトでは「https://localhost:8081/」となります。

  • AuthKey
    Azure DocumentDB Emulator では認証キーは固定で「C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==」と決められています。

  • Microsoft.Azure.Documents.Client.DocumentClient
    DocumentDBへの接続クライアント(繋ぎ役)は「DocumentClientクラス」となります。本サンプルでは Initialize() メソッド内で初期化を行っています。

  • Initialize() / CreateDatabaseIfNotExistsAsync() / CreateCollectionIfNotExistsAsync()
    データベースは、データ操作を行う前に初期作成を行う必要があります(RDBでいうところのCREATE DATABASE / CREATE TABLE)。これに該当する処理をCreateDatabaseIfNotExistsAsync() / CreateCollectionIfNotExistsAsync()で実装しています。両メソッドは Initialize() から呼び出されています。つまり、初期化処理として BlogPostRepository.Initialize() を呼び出す必要があります。

  • CreateBlobPostAsync(BlogPost blogPost)
    ブログポスト(ドキュメント)をデータベース コレクション」に追加します(RDBのINSERT)。

  • GetAllBlogPostsAsync()
    登録されたすべてのブログポスト(ドキュメント)を取得しています(RDBのSELECT)。

⑤main()の実装(リポジトリクラスの呼び出し)

用意したリポジトリクラス(BlogPostRepository)を利用して、BlogPostモデルクラスのデータベースへの出し入れ処理を実装します。
以下がコンソールアプリケーションのmain処理の実装になります。

// Program.cs
using System.Threading.Tasks;
using System.Collections.Generic;

using Serilog;

namespace DocDbExampleCoreConsole
{
  class Program
  {

    private IEnumerable<BlogPost> blogPosts = null;

    static void Main(string[] args)
    {
      Program program = new Program();

      // データベースを初期化
      program.InitializeDatabase();

      // データを挿入
      program.InitializeData().Wait();

      // データを抽出
      program.GetAllBlogPosts().Wait();

      // 抽出したデータをコンソール出力
      Log.Logger = new LoggerConfiguration()
        .WriteTo.Console()
        .CreateLogger();
      foreach (var blogPost in program.blogPosts)
      {
        Log.Information("{@BlogPost}", blogPost);
      }

      System.Console.ReadLine();
    }

    /// <summary>
    /// データベース・コレクションを初期化します。
    /// </summary>
    private void InitializeDatabase()
    {
      // データベース・コレクションを作成
      BlogPostRepository.Initialize();
    }

    /// <summary>
    /// BlogPostサンプルデータを作成します。
    /// </summary>
    /// <returns></returns>
    private async Task InitializeData()
    {
      // 50件、テスト投稿を作成
      for (int i = 0; i < 50; i++)
      {
        BlogPost blogPost = new BlogPost() {
          Title = string.Format("Title {0}", i),
          Contents = string.Format("Hi! This contents no is '{0}'.", i),
          Author = new User() { Name = "nanasi san" },
          };
        blogPost.Comments = new List<Comment>();
        blogPost.Comments.Add(new Comment() { Text = "Good job!!" });

        await BlogPostRepository.CreateBlobPostAsync(blogPost);
      }
    }

    /// <summary>
    /// すべてのBlogPostデータを取得します。
    /// </summary>
    /// <returns></returns>
    private async Task GetAllBlogPosts()
    {
      this.blogPosts = await BlogPostRepository.GetAllBlogPostsAsync();
    }
  }
}

mainクラス実装のポイントは以下の通りです。

  • program.InitializeDatabase();
    リポジトリークラスの呼び出しにより、データベース・コレクションを作成しています。

  • program.InitializeData().Wait();
    リポジトリークラスの呼び出しにより、BlogPostサンプルデータを50件作成しています。

  • program.GetAllBlogPosts().Wait();
    リポジトリークラスの呼び出しにより、データベースコレクションに登録された全てのBlogPostデータ抽出しています。

  • ログ出力
    抽出の結果として得られたデータは「IEnumerable<BlogPost>」型です。
    個別データである BlogPost オブジェクトをSerilogでコンソール出力しています。
    Serilogを使用する理由は、オブジェクトのログ出力に際し、子プロパティを含めた形で自動でJSON形式出力してくれる機能を活用する為です。

以下がDocDbExampleCoreConsoleコンソールアプリケーションの実行結果となります。

2016-12-08 02:37:27 [Information] BlogPost { Id: "43eb6293-c532-46dd-969d-e6e4258bcd7d", Title: "Title 0", Contents: "Hi・・This contents no is '0'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "86380c10-c45e-46e5-9320-a432be8e838f", Title: "Title 1", Contents: "Hi・・This contents no is '1'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "77350d85-6e11-49ac-9f52-d14729cb41b4", Title: "Title 2", Contents: "Hi・・This contents no is '2'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "e151fef2-74d8-4735-a89a-971603169c36", Title: "Title 3", Contents: "Hi・・This contents no is '3'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "580291de-c99a-4893-b416-48954847413a", Title: "Title 4", Contents: "Hi・・This contents no is '4'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "a80815e6-5d03-49e9-b12e-a2892f504dbd", Title: "Title 5", Contents: "Hi・・This contents no is '5'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
...以下省略

⑥「Azure DocumentDB Emulator」のExplorerからデータを確認

「Azure DocumentDB Emulator」の管理画面である「https://localhost:8081/_explorer/index.html#」から投入されたBlogPostデータを参照してみます。
以下の画面のようにデータを確認する事が出来ます。

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

ちなみにタスクトレイアイコン マウス右ボタン「Reset Data...」をクリックすると、簡単にきれいさっぱりデータが消去されるのでお気を付けください。

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

「Azure DocumentDB」から「Azure DocumentDB Emulator」にデータを(エクスポート)リストア

最後にクラウド上の Azure DocumentDB から Azure DocumentDB Emulator にデータリストアする手順を説明しておきます。

「本来の実装はクラウドのAzure DocumentDBで行っているが、出先等で一時的にローカルエミュレータ開発をしたい」
「今までEmulatorが無かったから、常にクラウドに接続していたけれど、クラウドエミュレータを平行利用開発したい」

等々の場合に有効かと・・・

以下のページから「Azure DocumentDB Data Migration Tool」をダウンロードします。

Download Azure DocumentDB Data Migration Tool from Official Microsoft Download Center

ダウンロードしたzipファイルを任意のフォルダに解凍し dtui.exe を起動します。

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

「Nextボタン」をクリックします。

表示された以下のウィンドウにて、Import from は「DocumentDB」、ConnectionString は「Azure上のDocumentDBへの接続文字列(Database=xxx も忘れずに)」、Collection は「インポート対象のコレクション名」を入力して「Nextボタン」をクリックします。

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

以下の画面において、export先(つまり、ここではローカルの Azure DocumentDB Emulator)の設定を行います。
ConnectionString は、EndPoint=「https://localhost:8081」・AccountLey=「固定値」Database=「任意(ここではBlog)」となります。
入力したら「Nextボタン」をクリックします。

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

以下のウィンドウが表示されたら「Next」ボタンをクリックします(必要であれば「エラーログファイル」の設定も行って)。

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

設定内容の最終確認を行う以下の画面が表示されいます。
問題なければ「Next」ボタンをクリックします。

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

以上で「クラウド Azure DocumentDB」→「ローカル Azure DocumentDB Emulator」へのデータのリストアが完了しました。

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

まとめ

NoSQLが登場したのはいつだったでしょうか・・・RedisやCassandra、memcashedに興奮し、迷走し・・・Azure黎明期にも当時のNoSQL(DoucmentDBという名称ではなかったと思う・・・)に興奮した記憶があります。
ビジネスアプリケーションにおいては、まだまだNoSQLの導入は遅れているように思います。同時にNoSQLがRDBにとって代わる技術では無い事も事実です。
RDBの堅牢なデータ管理とNoSQLのパフォーマンスを適材適所に組み合わせたシステムの開発、という提案を開発者として行っていきたいものです。

で、まあ新しい技術への探求は相変わらず楽しいのですよね^^

.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

では、またー。

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ソース管理やタスクランナーもサポートされています。
無償の開発環境ですが、かなり活用のしがいがあるソフトウェアだと思っています。