『SQL アンチパターン』を読んだ

本書は SQLアンチパターンを集めた、いわば『べからず集』。 良いパターンを集めた技術書はよく見かけるけど、 悪いパターンを集めたものは珍しい。

ただ、本書で扱っているアンチパターン自体は珍しいものではない。 データベースを使うアプリケーションを開発したことがあるエンジニアは、 本書が紹介するアンチパターンのどれかに陥った経験はあるんじゃないだろうか。

自分の場合は、擬似キーの ID の空き番号を再利用してしまい、 『シュードキー・ニートフリーク(疑似キー潔癖症)』 を患ったことがある。

また、Rails を使っていた名残でつい、 交差テーブルを含むすべてのテーブルに ID 列を追加してしまい、 『IDリクワイアド(とりあえずID)』も患ってしまっている。 こちらは現在進行形なのだけれど。

本書は、アンチパターンの紹介だけで終わらずに、 ちゃんと解決策も紹介している点が素晴らしい。 「愚者は経験に学び、賢者は歴史に学ぶ」というわけではないが、 まだ陥ったことのないアンチパターンとその解決法を知っておくことで、 未然に防げる可能性が高まる。

ある程度経験を積んだエンジニアが読むと 「このアンチパターンやったことあるあるw」となって面白い。 駆け出しの新人にだってお勧め。 むしろ必読。 本書のようにアンチパターンを集めた書籍が、 他の言語でも出てきたら面白いだろうな。

SQLアンチパターン

SQLアンチパターン

Windows10 で gem をインストールしようとしたら SSL のエラーが発生した

Middleman を Windows10 にインストールしようと思ったので

tnakamura.hatenablog.com

を参考に

  • RubyInstaller の Ruby2.3.1(x64)
  • DevKit-mingw64-64-4.7.2-20130224-1432-sfx.exe

をインストールしてみた。

インストールは何事もなく終了したので、次に bundler をインストールしようとしたら

D:\src>gem install --no-ri --no-rdoc bundler
ERROR:  Could not find a valid gem 'bundler' (>= 0), here is why:
          Unable to download data from https://rubygems.org/ - SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed (https://api.rubygems.org/specs.4.8.gz)

というエラーが発生。 SSL 証明書の検証に失敗しているけど、 rubygems が使っている SSL 証明書が古かったりするのか?

rubygems を更新すると解決するようだったので、 rubygems を更新するための gem をインストール。 https が使えないので http でアクセスするように指定しておく。

> gem install rubygems-update --source http://rubygems.org/

rubygems を更新。

> rubygems_update

2.6.7 に更新され、今度は

> gem install --no-document bundler

が成功した。

余談だけど、gem のドキュメントをインストールしないオプションは --no-ri --no-rdoc ではなく --no-document を使うようになったのか。 こっちの方がわかりやすくていいね。

ASP.NET Core で Swashbuckle

はじめに

Swashbuckle はまだプレリリースの段階ではあるけど、 ASP.NET Core にも対応しているみたい。

github.com

Swashbuckle を使えば、Web API の実装から Swagger Definitions を生成できる。

温かみのある手作業で、 Web API の Swagger Definitions を書いていたけど、 もう限界なので試してみることにした。

プロジェクト新規作成

今回は Web API だけ使うので、 ASP.NET Core の Web API プロジェクトを作成しておく。

Swashbuckle をインストール

project.jsondependencies

"Swashbuckle": "6.0.0-beta902"

を追加して、パッケージをリストア。

Swagger を使うように Startup を修正

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace SwaggerSample
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddMvc();

            services.AddSwaggerGen();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            app.UseMvc();

            // Swagger Definitions を出力できるようにする
            app.UseSwagger();

            // Swagger UI を表示できるようにする
            app.UseSwaggerUi();
        }
    }
}

モデルを定義

Web API で返すモデルだけでなく、 入力用のモデルと、 エラー用のモデルも定義してみた。

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

namespace SwaggerSample.Models
{
    public class Item
    {
        public string Id { get; set; }

        public string Title { get; set; }

        public string Description { get; set; }
    }

    public class ItemInputModel
    {
        [Required]
        public string Title { get; set; }

        [Required]
        public string Description { get; set; }
    }

    public class ErrorModel
    {
        public Dictionary<string, string> Errors { get; } = new Dictionary<string, string>();
    }
}

コントローラーを作成

プロジェクトを新規作成すると ValuesController ってのが作られるけど、 そいつは削除してしまって、 ItemsController を作成。

using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SwaggerSample.Models;

namespace SwaggerSample.Controllers
{
    [Route("api/[controller]")]
    public class ItemsController : Controller
    {
        private static readonly ConcurrentDictionary<string, Item> _items
            = new ConcurrentDictionary<string, Item>();

        [HttpGet]
        [ProducesResponseType(typeof(IEnumerable<Item>), 200)]
        public IActionResult Get()
        {
            return Ok(_items.Values);
        }

        [HttpGet("{id}")]
        [ProducesResponseType(typeof(Item), 200)]
        [ProducesResponseType(typeof(ErrorModel), 404)]
        public IActionResult Get(string id)
        {
            Item item;
            if (_items.TryGetValue(id, out item) == false)
            {
                return NotFound(new ErrorModel
                {
                    Errors =
                    {
                        [ "id"]= $"ID = {id} のアイテムは存在しません。"
                    }
                });
            }
            else
            {
                return Ok(item);
            }
        }

        [HttpPost]
        [ProducesResponseType(typeof(Item), 200)]
        [ProducesResponseType(typeof(ErrorModel), 400)]
        public IActionResult Post([FromBody]ItemInputModel inputModel)
        {
            if (ModelState.IsValid == false)
            {
                return BadRequest(CreateValidationError());
            }

            var item = new Item
            {
                Id = Guid.NewGuid().ToString(),
                Title = inputModel.Title,
                Description = inputModel.Description,
            };
            _items.TryAdd(item.Id, item);
            return Ok(item);
        }

        [HttpPut("{id}")]
        [ProducesResponseType(typeof(Item), 200)]
        [ProducesResponseType(typeof(ErrorModel), 400)]
        [ProducesResponseType(typeof(ErrorModel), 404)]
        public IActionResult Put(string id, [FromBody]ItemInputModel inputModel)
        {
            Item item;
            if (_items.TryGetValue(id, out item) == false)
            {
                return NotFound(new ErrorModel
                {
                    Errors =
                    {
                        ["id"] = $"ID = {id} のアイテムは存在しません。"
                    }
                });
            }

            if (ModelState.IsValid == false)
            {
                return BadRequest(CreateValidationError());
            }

            item.Title = inputModel.Title;
            item.Description = inputModel.Description;
            _items[id] = item;
            return Ok(item);
        }

        [HttpDelete("{id}")]
        [ProducesResponseType(typeof(Item), 200)]
        [ProducesResponseType(typeof(ErrorModel), 404)]
        public IActionResult Delete(string id)
        {
            Item item;
            if (_items.TryRemove(id, out item))
            {
                return Ok(item);
            }
            else
            {
                return NotFound(new ErrorModel
                {
                    Errors =
                    {
                        ["id"] = $"ID = {id} のアイテムは存在しません。"
                    }
                });
            }
        }

        private ErrorModel CreateValidationError()
        {
            var error = new ErrorModel();
            foreach (var pair in ModelState)
            {
                error.Errors.Add(
                    pair.Key,
                    string.Join(
                        Environment.NewLine,
                        pair.Value.Errors.Select(e => e.ErrorMessage)
                    )
                );
            }
            return error;
        }
    }
}

戻り値の型を IActionResult にする場合、 ProducesResponseTypeAttribute を使って、 Swashbuckle にモデルの型を教えてあげないといけない。 NotFound とか BadRequest とか扱いたいなら必須。

Swagger UI を表示

Visual Studio から実行したいところだけど、マシンパワーが貧弱で起動が遅いので、

> dotnet run

でサービスを実行。

ブラウザで localhost:5000/swagger/ui にアクセスしてみる。

f:id:griefworker:20161007165806p:plain

Swagger UI が表示された。 ここから Web API を実際に呼び出すことができて便利。

Swagger Definitions を表示

Swagger UI に Swagger Definitions の URL が表示されているので、 ブラウザでアクセスしてみる。

f:id:griefworker:20161007165821p:plain

Swagger Definitions の JSON が表示された。 モデルの情報もちゃんと含まれている。

おわりに

Swagger UI は Web APIデバッグに超便利だった。 Swagger Definitions も生成してくれるので、 Swagger Codegen に渡せばクライアントのソースコードまで生成できる。

Swagger Codegen のインストールが面倒なら、 公式がホストしている Swagger Editor に貼り付けて生成する手もある。

Swagger Definitions を YAML でゴリゴリ書いていたけど、 Web API からの自動生成を経験してしまったので、 もう戻れないな。

PAOPAO

昼の弁当が食べ足りなかったから、 三越地下2階にある PAOPAO で肉まんを買ってしまった。 肉まんを食べるのは大宰府観光以来だ。

生地はふわもち。特に餡が美味いね。 豚肉に刻んだ椎茸と筍が入ったオーソドックスな餡だけど、 これで良い。これが良い。 凝った具やこぼれるほどの肉汁はいらない。 シンプルイズベスト。

実に好みの味で、ぺろりと平らげてしまった。 あと1個はいけたな。

関連ランキング:デリカテッセン | 西鉄福岡駅(天神)天神駅天神南駅

証明書ファイルを読み込んで WCF の SSL over TCP で使う

WCF は NetTcpBinding を使う場合でも SSL で通信を暗号化できる。

暗号化に使う SSL 証明書は、Windows の証明書ストアにインストールしてある中から検索して使うのが一般的なやり方っぽいが、 インストールしていない証明書のファイルを読み込んで使うことも可能。

コードはこんな感じ。WCF サービスをセルフホストするのに、Topshelf を使っている。

using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using Topshelf;

namespace WcfSample.Service
{
    class Program
    {
        static void Main(string[] args)
        {
            HostFactory.Run(config =>
            {
                config.Service<ServiceHost>(service =>
                {
                    service.ConstructUsing(settings =>
                    {
                        var certPath = Path.Combine(
                            AppDomain.CurrentDomain.BaseDirectory,
                            "cert",
                            "certfile.pfx");

                        // 証明書ストアに証明書が一時的に格納されるので、
                        // 格納場所を指定しておかないとデフォルトの場所に格納されてしまう。
                        // その場合管理者権限が必要になってしまうので、
                        // 現在のユーザーのストアに格納するように指定した。
                        var cert = new X509Certificate2(certPath, "password", X509KeyStorageFlags.UserKeySet);

                        var host = new ServiceHost(typeof(SampleService));
                        host.Credentials.ServiceCertificate.Certificate = cert;
                        return host;
                    });
                    
                    service.WhenStarted(host => host.Open());

                    service.WhenStopped(host =>
                    {
                        host.Close();
                    });
                });

                config.RunAsNetworkService();
                config.SetDescription("WCF サンプルサービス");
                config.SetDisplayName("WCF サンプルサービス");
                config.SetServiceName("WcfSampleService");
            });
        }
    }
}

アプリケーション構成ファイルも載せておく。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
    </startup>
    <system.serviceModel>
        <bindings>
            <netTcpBinding>
                <binding name="SampleServiceTcp">
                    <security mode="Transport">
                        <transport clientCredentialType="None" protectionLevel="EncryptAndSign" />
                    </security>
                </binding>
            </netTcpBinding>
        </bindings>
        <services>
            <service name="WcfSample.Service.SampleService">
                <endpoint address="net.tcp://localhost:808/SampleService" binding="netTcpBinding"
                    bindingConfiguration="SampleServiceTcp" contract="WcfSample.Common.ISampleService" />
            </service>
        </services>
    </system.serviceModel>
</configuration>

アンリ・シャルパンティエ

大丸地下2階に出店している『アンリ・シャルパンティエ』でケーキを買って帰った。 ここの『Wチーズケーキ』は食べたことがあるけど、 今回のお目当ては『ミルクレープ』と『ザ・ショートケーキ』の2つ。

『ミルクレープ』は層が細かくて見た目が美しい。 間に挟まっているのは、ただの生クリームでは無い気がする。 クリームチーズが入っているのかもしれない。 究極のミルクレープというだけあって美味かった。

『ザ・ショートケーキ』は粒の大きな苺が3つものっていて見た目が豪華。 スポンジの間のクリームの層にもたくさん。 店で売っているショートケーキで、こんなに苺が使われたものは初めてだ。 こちらも生クリームが美味い。 今まで食べたショートケーキの中で1・2位を争う美味さだった。

どちらのケーキも期待以上で大変満足。 ケーキ2つで1000円オーバー、 ショートケーキにいたっては過去最高の値段だったけど、 それだけ払ってでもまた食べたいと思える味だった。

関連ランキング:ケーキ | 天神南駅西鉄福岡駅(天神)天神駅

WCF サービスを Topshelf で Windows サービス化

はじめに

WCF サービスをセルフホストする場合、 Windows サービスを作ることになると思う。

本番環境で動かすときは Windows サービスでいいんだけど、 デバッグはコンソールアプリケーションの方が便利。 そのため、Windows サービスとコンソールアプリケーションのプロジェクトを両方作って保守している。 もちろんコードの大部分は共有しているが。

Topshelf 使えば手軽に Windows サービスを作れる上、 コンソールアプリケーションとしても実行できるらしい。 こいつを使えば2つのプロジェクトを保守する手間とオサラバできそうだ。 これは試してみる価値あるな。

github.com

Topshelf をインストール

Visual Studio の Package Manager Console で

> Install-Package Topshelf

を実行し、WCF サービスのプロジェクトに Topshelf をインストールする。

Topshelf を使って WCF サービスをセルフホスト

下記のようなサービスコントラクトとサービスクラスがあったとする。

namespace WcfSample.Common
{
    using System.ServiceModel;

    [ServiceContract]
    public interface ISampleService
    {
        [OperationContract]
        string GetServerName();
    }
}

namespace WcfSample.Service
{
    using System;
    using WcfSample.Common;

    public class SampleService : ISampleService
    {
        public string GetServerName()
        {
            return Environment.MachineName;
        }
    }
}

Topshelft を使えば、こんな感じで Windows サービス化できる。

namespace WcfSample.Service
{
    using System.ServiceModel;
    using Topshelf;

    class Program
    {
        static void Main(string[] args)
        {
            HostFactory.Run(config =>
            {
                config.Service<ServiceHost>(service =>
                {
                    // エンドポイントの設定はアプリケーション構成ファイルで行うことを想定しているので
                    // ここでは設定していない。
                    service.ConstructUsing(settings => new ServiceHost(typeof(SampleService)));
                    service.WhenStarted(host => host.Open());
                    service.WhenStopped(host => host.Close());
                });

                config.RunAsNetworkService();
                config.SetDescription("WCF サンプルサービス");
                config.SetDisplayName("WCF サンプルサービス");
                config.SetServiceName("WcfSampleService");
            });
        }
    }
}

コンソールアプリケーションとして実行

作成した exe を普通に実行するだけ。

> WcfSample.Service.exe

Windows サービスとしてインストール

install サブコマンドが使えるようになっているので、

> WcfSample.Service.exe install

Windows サービスをインストールできる。

インストールしただけでは開始されていないので、 start サブコマンドを使って

> WcfSample.Service.exe start

Windows サービスを開始してやる必要がある。

Windows サービスをアンインストール

uninstall サブコマンドも使える。 今回のサンプルなら

> WcfSample.Service.exe uninstall

でアンインストールできた。

おわりに

超簡単に WCF サービスを Windows サービスでホストできた。 この簡単さを経験した後だと、 もう Windows サービスのプロジェクトテンプレートを使いたいとは思えないな。 今まで仕方なく使っていたけど、もうやってられない。