TTYF ~earlgrey の雑記~

主に自分用メモとか

CodeBuild をローカルで動作確認する(Windows/Mac 対応版)

AWS CodeBuild では buildspec.yml という YAML ファイル(詳細はこちら)にビルドの詳細を定義しますが、これを用いて、 AWS 上でなくてもローカル環境でビルドを行うことができます。そのため、ローカルで十分に動作確認を行ってから AWS 上で本稼働させることができます。今回はこの方法を解説していきます。

Docker のインストール

CodeBuild は Docker コンテナ上でビルドを行いますので、ローカルで動かすのにも Docker を使います。なので、Docker が入っていなければインストールします。

WindowsMac では、無料で使用できる Docker の動作環境として、Docker Desktop と Docker Toolbox の 2 種類が存在しています。ダウンロードはこちら。

Docker Toolbox

Releases · docker/toolbox · GitHub

Desktop の方が新しく、Hyper-V や Hypervisor.framework といった OS 標準のハイパーバイザを使用しておりパフォーマンスもおそらく高いですが(一方 Toolbox は VirtualBox ベースです)、動作環境も少し厳しめになっています。一例を挙げると、Windows 版は Windows 10 64bit Pro 以上、Mac 版は macOS Sierra 以上、といった感じです。詳しくはそれぞれ以下の「System Requirements」をご覧ください。

Install Docker Desktop on Windows | Docker Documentation
Install Docker Desktop on Mac | Docker Documentation

もうひとつの注意点として、Docker Desktop は無償ではありますが、メールアドレスによるアカウント取得とそれによるログインが必要です。これについては、かつて GitHub の Issue にログインなしで使わせてほしい、という要望が挙がったものの却下されています。ちなみに以下がその Issue ですが、めちゃ炎上してます。Thumbs Down 4,600 超なんてそうそうお目にかかれない...!

Download Docker CE without logging in · Issue #6910 · docker/docker.github.io · GitHub

話が逸れましたが、Windows/Mac で Desktop の動作環境を満たしていなかったり、アカウント取得が面倒だったりする場合は Toolbox を使うことになります。詳しくはこちら。必要な動作環境はそれぞれ以下の「Step 1: Check your version」をご覧ください。Desktop よりは緩くなっています。

Install Docker Toolbox on Windows | Docker Documentation
Install Docker Toolbox on macOS | Docker Documentation

Toolbox を使う場合、いくつか注意点があります。

Windows の場合は、Hyper-V をオフにした方が無難です。VirtualBoxHyper-V と競合するためで、VirtualBox 6.0 からは一応共存可能になったようですが、Docker Toolbox にバンドルされているのは現時点で 5.2.20 です。Docker Toolbox よりも先に 6.0 をインストール、もしくは後から 6.0 に上げることももちろんできますが、少し手間がかかるのと、あとは環境によっては 6.0 でも結局 Hyper-V と共存できないことがあるようです。以下参考です。

VirtualBox 6.0 から Hyper-V と共存できるはず - Qiita

Mac の場合、Sierra/High Sierra/Mojave では、標準のインストーラが動作しません。代わりに Homebrew Cask でインストールします。詳細は省略しますが、このあたりを参考にしてください。

macOSのSierraではDocker Toolboxのインストールが出来ない - Qiita
macOS Sierra Docker Toolbox のインストーラがエラるので Brew cask でインストールした - Qiita

最後に Linux ですが、Linux は Desktop/Toolbox のような区別はありません。基本的にディストリビューション標準のパッケージマネージャでインストールします。詳しくはこちらを。

Get Docker Engine - Community for CentOS | Docker Documentation
Get Docker Engine - Community for Debian | Docker Documentation
Get Docker Engine - Community for Fedora | Docker Documentation
Get Docker Engine - Community for Ubuntu | Docker Documentation

AWS CodeBuild の Docker イメージ

CodeBuild をローカル実行するために必要なものは GitHub で公開されています。

github.com

上のリポジトリを次のコマンドでクローンする(GitHub アカウントが必要)か、ZIP でダウンロードして展開します。

$ git clone https://github.com/aws/aws-codebuild-docker-images.git

このリポジトリには次のものが含まれています。

  • ビルドを実行する Docker コンテナのイメージをビルドするための定義ファイル(Dockerfile)
  • Docker コンテナを起動し、ビルドを実行するためのサポートスクリプト

Docker でビルドを実行するコンテナの元になるイメージファイルは含まれておらず、それは Dockerfile から自前でビルドする必要があります。
ここでビルドという言葉が2回出てきてややこしいですが、それぞれが指すビルドの成果物は、前者のビルドがアプリケーション、後者のビルドが Docker のイメージファイルです。この先は便宜上前者をビルド、後者を構築というふうに使い分けていきます。

さらに、CodeBuild をローカルで動かすには、ビルドを実行するコンテナのほかに、CodeBuild エージェントというコンテナが必要です。
ホスト OS の Docker 上でいきなりビルド用のコンテナを実行するわけでなく、まずは CodeBuild エージェントのコンテナを実行、CodeBuild エージェントはさらにビルド用のコンテナを起動し、それをリモート操作する、という多段かつ並列的な構成で動作します(このような手法を Docker outside of Docker というらしいです)。こうしている理由や利点は不勉強なのでよくわかりませんが、CI 環境ではわりとよく使われるらしいです。

というわけで、必要な Docker イメージは以下の 2 つになります。

  • 実際にビルドを行うコンテナのイメージ
  • CodeBuild エージェントのイメージ

それぞれ、準備する方法を説明していきます。

ビルドを行うコンテナのイメージ構築

ビルドを実行するコンテナのイメージは、前述のように Dockerfile の定義に従って構築します。

構築対象のイメージ選択

まずはどのような Docker イメージを構築するのか決める必要があります。既に作成されている CodeBuild のプロジェクトがあって、それをローカルでビルドしたいのであれば、プロジェクトで使用しているイメージをコンソールなどで確認して、それと同じものにします。以下の例だと、「aws/codebuild/standard:2.0」がイメージの種類を表しています。

f:id:s_earlgrey:20190812201136p:plain

これから新たに CodeBuild プロジェクトを作成するのであれば、基本的に「aws/codebuild/standard:2.0」または「aws/codebuild/amazonlinux2-x86_64-standard:1.0」にしておけば良いでしょう(長いので、以降は適宜 aws/codebuild/ を略します)。これらには .NET Core 2.2、Python 3.7 など複数の処理系のビルド/実行環境が予め含まれています。処理系の完全なリストはこちらにあります。(※言語が日本語で開いた場合はリストが表示されませんので、English に切り替えてください。)

Docker Images Provided by CodeBuild - AWS CodeBuild

「standard:2.0」」も「amazonlinux2-x86_64-standard:1.0」も含まれる処理系はほぼ同じなのですが、唯一 Java だけが、前者は オリジナルの OpenJDK、後者は Amazon Corret というように異なっています。あとは OS が前者は Ubuntu 18.04、後者は Amazon Linux 2 です。
このあたりは要件に合わせてイメージを選択してください。あくまでビルド環境ですので、OS の違いには特にシビアにならなくても良いと思います。

構築するイメージを決めたら、先ほどクローン or ダウンロードしたファイルから、目的のイメージの Dockerfile があるディレクトリを探します。「standard:2.0」の場合は ubuntu/standard/2.0、「amazonlinux2-x86_64-standard:1.0」の場合は al2/x86_64/standard/1.0 です。
それ以外で、「aws/codebuild/dot-net:core-2.1」などのように名前に処理系が含まれているイメージのものは、ubuntu/unsupported_images 配下にあります。もし既存の CodeBuild プロジェクトで使用しているのとまったく同じバージョンが見当たらない場合は、用意されている中でひとつ次のバージョンを使うか、いっそ既存プロジェクトの buildspec.yml を「standard:2.0」や「amazonlinux2-x86_64-standard:1.0」用に移行できないか検討してみるのも良いでしょう。

イメージの構築

この先はいくつか docker コマンドを使用します。それにあたって、環境に応じて以下の前提事項がありますので、これに従ってください。

  • Docker Toolbox を使用している場合、Docker Quickstart Terminal で起動したターミナルを用いる
  • Linux を使用している場合、sudo で実行する

イメージの構築は、次のコマンドで行います。

$ (sudo) docker build -t [任意の名称:タグ] [Dockerfile のあるディレクトリのパス]

-t オプションに続ける名称は何でも良いですが、CodeBuild 用イメージ種類の識別子、つまり「aws/codebuild/standard:2.0」などをそのまま使うのがわかりやすいでしょう。

イメージ構築にはそれなりに時間がかかります。「standard:2.0」であれば、CPU パワーやネットワークの帯域にもよりますが 30 分~ 1 時間半程度です。
出力は大量にあるので省略しますが、完了するとこのようなメッセージが表示されます。

Successfully built ffcd726be2dd
Successfully tagged aws/codebuild/standard:2.0
SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.

最後の SECURITY WARNING は、Windows で実行した場合に表示されますが、結論から言うと無視して良いです。
Dockerfile の中で、COPY や ADD という、Docker のホスト OS から構築中の Docker イメージにファイルをコピーするコマンドが使えるのですが、これでコピーされたファイルのパーミッションが Docker イメージの中では一律 '-rwxr-xr-x' にセットされているので、これが過剰であれば訂正するように、ということを言っています。
システムの本番環境として使うイメージであればそのあたりは厳密に取り扱うべきですが、これはあくまでローカルでアプリケーションのビルドを行うためのイメージですので、特に気にする必要もないでしょう。
余談ですが、Dockerfile で COPY や ADD を使っていなくても、構築に使用した環境が Windows であればこのメッセージが出ます。

さて、実際に構築されたイメージを見てみましょう。

$ (sudo) docker images
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
aws/codebuild/standard   2.0                 ffcd726be2dd        4 minutes ago       7.36GB
ubuntu                   18.04               4c108a37151f        2 weeks ago         64.2MB

aws/codebuild/standard 2.0」というのが今回構築したイメージです。その下にある「ubuntu 18.04」は、今回構築したもののベースになっているイメージで、構築の最初のステップで自動的にダウンロードされたものです。

CodeBuild エージェントのダウンロード

CodeBuild エージェントは、出来合いのものをダウンロードしてくるだけです。次のコマンドでダウンロードします。

$ (sudo) docker pull amazon/aws-codebuild-local:latest --disable-content-trust=false

もう一度イメージのリストを確認してみましょう。「amazon/aws-codebuild-local latest」が追加されていますね。

$ (sudo) docker images
REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
aws/codebuild/standard       2.0                 ffcd726be2dd        8 minutes ago       7.36GB
amazon/aws-codebuild-local   latest              b0bdf3d66f0e        7 weeks ago         563MB
ubuntu                       18.04               4c108a37151f        2 weeks ago         64.2MB

ビルドの実行

ここまでで準備が整いました。CodeBuild をローカルで実行するためには、GitHub からクローン or ダウンロードしてきたリポジトリの local_builds/codebuild_build.sh を使います。このスクリプト内部で docker コマンドを使用しているので、ここでも Toolbox の場合は Docker Quickstart Terminal を使い、Linux の場合は sudo してください。
基本的なコマンドは以下の通りです。

$ (sudo) codebuild_build.sh -i [docker イメージの名称:タグ] -a [ビルド結果出力先のディレクトリ] -s [ソースコードのあるディレクトリ] (-b [buidspec のパス])

-b オプションは省略可能で、その場合は -s オプションで指定したディレクトリ直下の buildspec.yml が使用されます。ほかにもいろいろオプションがありますが、詳しくはリポジトリの local_builds/README.md をご覧ください。

WindowsMac の場合、-a, -s, -b の各オプションには、以下の表にあるディレクトリ配下を指定してください。これ以外はデフォルト状態ではビルドコンテナがディレクトリやファイルを認識できません。然るべき設定をすればその他のディレクトリも使えますが、デフォルトでも特に困ることはないと思いますので、その方法は省略します。

OS Docker 使用可能ディレクト
Windows Desktop C:\ 配下
Windows Toolbox C:\Users 配下
OS X/macOS Desktop /Users または /Volumes 配下
macOS Toolbox /Users 配下

Windows の場合、さらに注意点があります。Desktop と Toolbox で実行方法が異なりますので、別々に書いていきます。

Windows + Docker Desktop の場合

codebuild_build.sh は Bash スクリプトなので、Windows では別途 Bash の実行環境を導入する必要があります。Git for Windows に付属の Git Bash を使うのがお手軽で良いと思いますので、その前提で書きます。WSL や Cygwin など他の Bash 環境の場合は未検証です。

  • ディレクトリのパス指定方法が、通常の Windows の記述と異なります。例えば C:\Users\username\source であれば、/c/Users/username/source になります。
  • コマンドを実行すると、いきなりエラーになります。ですが、最終的に実行される docker コマンドが出力されるので、その先頭に "winpty" を付けて実行してください。

実際に Git Bash で実行すると、こんなふうに出力されます。

$ ./codebuild_build.sh -i aws/codebuild/standard:2.0 -a /c/Users/username/artifacts -s /c/Users/username/source
Build Command:

docker run -it -v //var/run/docker.sock:/var/run/docker.sock -e "IMAGE_NAME=aws/codebuild/standard:2.0" -e "ARTIFACTS=//C/Users/username/artifacts" -e "SOURCE=//C/Users/username/source" -e "INITIATOR=username" amazon/aws-codebuild-local:latest

the input device is not a TTY.  If you are using mintty, try prefixing the command with 'winpty'

ここから "docker run ..." をコピーして、改めてこのように入力します。これでビルドが実行できます。

$ winpty docker run -it -v //var/run/docker.sock:/var/run/docker.sock -e "IMAGE_NAME=aws/codebuild/standard:2.0" -e "ARTIFACTS=//C/Users/username/artifacts" -e "SOURCE=//C/Users/username/source" -e "INITIATOR=username" amazon/aws-codebuild-local:latest

Windows + Docker Toolbox の場合

Windows + Docker Toolbox だと、私が試した限りではビルドコンテナに Windows 側のディレクトリを認識させること自体ができません。仕方ないので、VirtualBox 上で動いている、Docker コンテナをホストしている Linux にログインして、そこでスクリプトを実行します。まずは Docker Quickstart Terminal で以下のコマンドを実行してください。

$ docker-machine ssh default

こんな風に表示がされればOKです。コンテナホストの Linux に入れました。

   ( '>')
  /) TC (\   Core is distributed with ABSOLUTELY NO WARRANTY.
 (/-_--_-\)           www.tinycorelinux.net

docker@default:~$

続けて codebuild_build.sh を実行してください。Linux では、Windows の C:\Users が /C/Users にマウントされていますので、その前提で各オプションのディレクトリを指定してください(codebuild_build.sh 自体も C:\Users 配下においておく必要があります)。

ビルドのサンプル

では実際にビルドしてみます。サンプルとしては、.NET Core で new しただけのコンソールアプリケーションを使います。

using System;

namespace hello
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.2</TargetFramework>
  </PropertyGroup>

</Project>

buildspec はこんな感じで。

version: 0.2
phases:
  install:
    runtime-versions:
      dotnet: 2.2
  build:
    commands:
      - dotnet publish -c Release
artifacts:
  files:
    - '**/*'
  base-directory: bin/Release/netcoreapp2.2/publish

これらをビルドします。ちなみに Windows + Docker Desktop で実行しています。

$ ./codebuild_build.sh -i aws/codebuild/standard:2.0 -a /C/codebuildtest/artifacts -s /C/codebuildtest 

Build Command:

docker run -it -v //var/run/docker.sock:/var/run/docker.sock -e "IMAGE_NAME=aws/codebuild/standard:2.0" -e "ARTIFACTS=//Ccodebuildtestartifacts" -e "SOURCE=//Ccodebuildtest" -e "INITIATOR=username" amazon/aws-codebuild-local:latest

the input device is not a TTY.  If you are using mintty, try prefixing the command with 'winpty'

$ winpty docker run -it -v //var/run/docker.sock:/var/run/docker.sock -e "IMAGE_NAME=aws/codebuild/standard:2.0" -e "ARTIFACTS=//C/codebuildtest/artifacts" -e "SOURCE=//C/codebuildtest" -e "INITIATOR=username" amazon/aws-codebuild-local:latest

...
agent_1  | [Container] 2019/09/01 13:55:33 Phase context status code:  Message:
agent-resources_build_1 exited with code 0
Aborting on container exit...

最後に Aborting とか出ていますが、その前に exited with code 0 と出ていれば問題ありません。出力先に指定したディレクトリを見てみましょう。

f:id:s_earlgrey:20190901231410p:plain


それらしいファイルができていますね。これを展開してみます。

f:id:s_earlgrey:20190901231446p:plain


確かにビルドされています。さらに実行できるか確認します。

C:\codebuildtest\artifacts\artifacts>dotnet hello.dll
Hello World!

ちゃんと実行できますね!ビルドは問題なく成功しました。

VB の論理演算における三値論理に注意

この記事は、Visual Basic Advent Calendar 2016 の 23 日目のエントリーです。22 日目は sutefu さんの なんちゃって仮想労働者を作る2 でした。

三値論理とは

最初に、そもそも三値論理とは何か、というところの説明をします。通常プログラミング言語において、条件判定等で用いる論理値としては、真 (true)/偽 (false) の 2 つがよく用いられていますが、これにもうひとつ不明 (unknown) を加えた 3 つの値で論理体系を構成しているものが三値論理です。
SQL ではわりと馴染み深いとともに、直感的でないところがあるがゆえにハマりやすいポイントでもあります。

具体的に、三値論理での演算結果がどのようになるかを表で記します。これを真理値表といいます。

NOT
true false
false true
unknown unknown

AND true false unknown
true true false unknown
false false false false
unknown unknown false unknown

OR true false unknown
true true true true
false true false unknown
unknown true unknown unknown

XOR true false unknown
true false true unknown
false true false unknown
unknown unknown unknown unknown

NOT はともかくとして、他のやつはパッと見いまいち覚えづらいですね。なるほど直感的じゃあない...。
ただ、AND だと false→unknown の順で強く、OR だと true→unknown の順で強く、XOR だと unknown が強い、と覚えておけば導きやすいかもしれません?

Nullable 型と三値論理

実は .NET においても、Nullable 型、というか Nullable(Of Boolean)(C# だと Nullable<bool>)とともに三値論理が登場しました。動作については前述の真理値表の unknown を Nothing(C# だと null)に置き換えたものと同じですが、一応 VB 表記でもう一度書いておきます。

Not
True False
False True
Nothing Nothing

And/AndAlso True False Nothing
True True False Nothing
False False False False
Nothing Nothing False Nothing

Or/OrElse True False Nothing
True True True True
False True False Nothing
Nothing True Nothing Nothing

XOr True False Nothing
True False True Nothing
False True False Nothing
Nothing Nothing Nothing Nothing

VB における三値論理

さてここからが本題です。この三値論理ですが、VB においては非常に非直感的で凶悪な振る舞いをしますので、良く理解しておかないと SQL の三値論理並みにハマります。この先は注意すべきポイントについて、私が把握している限り紹介していきます。
なお、これらは VB 固有の問題(?)で、同じ .NET 系の言語でも C# には当てはまらなかったりします。今回は VB の Advent Calendar なので C# には触れませんが...。

制御文における三値論理

If 等の制御文において、Nothing は False と同等に扱われます。そして何より重要なのが、Not Nothing は True ではない、ということです。例えば次のコードの実行結果は、どちらも「不一致」を出力します。

Dim b As Boolean? = Nothing

If b Then
    Console.WriteLine("一致")
Else
    ' Nothing は True ではないのでこちら
    Console.WriteLine("不一致")
End If

If Not b Then
    Console.WriteLine("一致")
Else
    ' Not Nothing はやはり Nothing であり True ではないのでこちら
    Console.WriteLine("不一致")
End If

このあたりは MSDN のリファレンスでも説明が十分でなかった時期が存在していて、例えば While だと Visual Studio 2005 の時点で明記されていましたが、If だと Visual Studio 2012 になってやっと明記されました。

Nullable 型との比較演算子は Nullable(Of Boolean) を返す

これも注意が必要で、個人的には凶悪度ではさっきのより上だと思っています。特に直感的でなくハマりやすいのが、True/False と Nothing の等値・非等値比較も Nothing が返ってくる(True/False ではない)こと、さらには Nothing 同士の比較も常に Nothing であることでしょうか。例えば次のコードの実行結果も、やはりすべて「不一致」を出力します。マジですか...。

Dim b As Boolean? = Nothing

' 比較 1
If b <> True Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

' 比較 2
If Not (b = True) Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

' 比較 3
If b <> False Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

' 比較 4
If Not (b = False) Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

' 比較 5
If b = b Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

' 比較 6
If b <> b Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

これは最新の Visual Studio 15 のリファレンス にも書かれていません。
それより問題なのが、この動作が Equals メソッドと整合性が取れていないことでしょうか。先ほどのサンプルコードを Equals メソッドを使って書き換えてみましょう。

Dim b As Boolean? = Nothing

' 比較 1,2
If Not b.Equals(True) Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

' 比較 3,4
If Not b.Equals(False) Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

' 比較 5
If b.Equals(b) Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

' 比較 6
If Not b.Equals(b) Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

この実行結果は、比較 6 のみ「不一致」、他は「一致」が出力されます。これなら直感的ですし、比較演算子を使った場合でも同じ結果になってほしいものですが...。

内部的な仕掛け

なぜこのように比較演算子と Equal メソッドの不整合が起きているかというと、VB コンパイラが比較文を書き換えているからです。
次のコードは、2 つ前のサンプルコードを一旦 Visual Studio 2015 でコンパイルしてから ILSpy で逆アセンブルしたものです。

Dim b As Boolean? = Nothing

' 比較 1
Dim valueOrDefault As Boolean = (Not b).GetValueOrDefault()
If valueOrDefault Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

Dim flag As Boolean? = b

' 比較 2
Dim flag2 As Boolean? = If(flag.HasValue,
                           New Boolean?(flag.GetValueOrDefault()),
                           Nothing)
Dim valueOrDefault2 As Boolean = (If(flag2.HasValue,
                                     New Boolean?(Not flag2.GetValueOrDefault()),
                                     flag2)).GetValueOrDefault()
If valueOrDefault2 Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

' 比較 3
flag2 = b
Dim valueOrDefault3 As Boolean = (If(flag2.HasValue,
                                  New Boolean?(flag2.GetValueOrDefault()),
                                  Nothing)).GetValueOrDefault()
If valueOrDefault3 Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

' 比較 4
flag2 = Not b
Dim valueOrDefault4 As Boolean = (If(flag2.HasValue,
                                     New Boolean?(Not flag2.GetValueOrDefault()),
                                     flag2)).GetValueOrDefault()
If valueOrDefault4 Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

' 比較 5
Dim valueOrDefault5 As Boolean = (If((b.HasValue And b.HasValue),
                                  New Boolean?(b.GetValueOrDefault() = b.GetValueOrDefault()),
                                  Nothing)).GetValueOrDefault()
If valueOrDefault5 Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

' 比較 6
Dim valueOrDefault6 As Boolean = (If((b.HasValue And b.HasValue),
                                     New Boolean?(b.GetValueOrDefault() <> b.GetValueOrDefault()),
                                     Nothing)).GetValueOrDefault()
If valueOrDefault6 Then
    Console.WriteLine("一致")
Else
    Console.WriteLine("不一致")
End If

うわあ、なんだか凄いことになっちゃったぞ。
でも内容を理解する必要はなくて、三値論理を実現するために何やら面倒なことをやっているんだな、ぐらいの認識で良いと思います。

わざわざここまでして SQL でも賛否が別れる三値論理を取り込まなくていいのに、という気がしています。Equals および等値演算子 (==) 実装のガイドライン に書かれている、「等値演算子 (==) を実装する場合は、常に Equals メソッドをオーバーライドし、同じ処理を実行させます。」という基準にも反していますし。

Dictionary のキーとしては一致と見做されるのに等値演算子では別物として扱われるということが起きえるわけで、結構問題なはずなんですが、なぜこんな言語仕様になっているのか...。条件文や比較演算子で三値論理を適用しない Option を新設してほしい、みたいなのって、Roslyn へのプルリクエストではなく単なる要望として出せないものでしょうか。

LINQ での取り扱い

LINQ to Objects で、Nullable 型のメンバを含んだエンティティクラスに対して操作を行うこともあるかと思いますが、そんなときにも Nothing との比較は Nothing であることを頭に入れておく必要があります。<> 演算子を用いるときは要注意です。

まずはクエリ式の場合から行ってみましょう。

Dim numbers As Integer?() = {1, 2, Nothing, 3}
Dim filtered = From number In numbers Where number <> 2 Select number

Console.WriteLine(String.Join(",", filtered))

このコードの出力結果は「1,3」になります。「1,,3」ではありません。2 と一緒に Nothing も除外されているわけです。

次に拡張メソッドで書いてみましょう。

Dim numbers As Integer?() = {1, 2, Nothing, 3}
Dim filtered = numbers.Where(Function(x) x <> 2)

Console.WriteLine(String.Join(",", filtered))

こちらは Ontion Strict の設定によって結果が変わってきます。
まず Option Strict On の場合、こちらはコンパイルエラーが発生します。問題なのは Where メソッドの引数で、ここには Func(Of TSource, Boolean) が要求されるのに、実際は Func(Of TSource, Boolean?) が与えられているためです。エラーメッセージは「Option Strict On では 'Boolean?' から 'Boolean' への暗黙的な変換は許可されていません。」と出力されます。
次に Option Strict Off の場合は、コンパイルは通りますが InvalidOperationException が発生します。Where メソッドの引数が暗黙のうちに Function(x) x.Value <> 2 と書き換えられるため、Nothing を処理しようとしたときに Value を参照できずにエラーになる、といった具合です。

エラーを発生させないためには、次のように書く必要があります。これの出力結果は、ひとつめが「1,,3」、ふたつめが「1,3」です。

Dim numbers As Integer?() = {1, 2, Nothing, 3}

' 結果に Nothing を含める場合
Dim filtered = numbers.Where(Function(x) Not x.HasValue OrElse x.Value <> 2)
Console.WriteLine(String.Join(",", filtered))

' 結果に Nothing を含めない場合
Dim filtered2 = numbers.Where(Function(x) x.HasValue AndAlso x.Value <> 2)
Console.WriteLine(String.Join(",", filtered2))

ちなみに C# では、同じように書いたつもりでも結果が異なるので、これまた注意が必要です。下記のようにクエリ式、拡張メソッドどちらを用いても出力結果は「1,,3」です。

int?[] numbers = {1, 2, null, 3};

// クエリ式
var filtered = from number in numbers where number != 2 select number;
Console.WriteLine(String.Join(",", filtered));

// 拡張メソッド(HasValue をチェックしなくてもエラーにならない)
var filtered2 = numbers.Where(x => x != 2);
Console.WriteLine(String.Join(",", filtered2));

あ、最初に C# には触れないといったのに触れちゃいました。それはさておき、これ、C#er が何らかの理由で VB を書くことになったときにめちゃ戸惑うと思います。やっぱり三値論理を無効化する Option の登場を強く望みます。

VB でインターフェイスの明示的実装

この記事は、Visual Basic Advent Calendar 2016 の 17 日目のエントリーです。16 日目は mmYYmmdd さんの VBAHaskell での関数定義 でした。

今回紹介するネタは特に目新しいものではないのですが、VBインターフェイスの明示的実装方法を調べるときのググラビリティ向上のために選んでみました。もっとも英語でググる と I'm Feeling Lucky 一発で解答にたどり着けたりしますけど。

現状ググラビリティがもうひとつなのは、Microsoft がなぜか VB においてはインターフェイスの明示的実装という用語を使っていないからです。理由はよくわかりませんが...。

インターフェイスの明示的実装とは

そもそも明示的なインターフェイスの実装って何?ということを念のため確認しておくと、あるインターフェイスのメンバに対して、実装しているクラスの参照変数ではなく、インターフェイスの参照変数を通じてしか呼び出すことができないようにすることです。言葉だけだとちょっとわかりにくいですが、後でサンプルを出します。

どういうときに使うかというと、フレームワークやライブラリで呼び出すために必要なんだけど、ユーザーコードからは使ってほしくないメンバーを実装するときが多いでしょうか。例えば ICloneable とかですかね。詳しい指針は、古い文書ですが MSDN の解説 をご覧ください。

VB での書き方

VBインターフェイスの明示的実装を行うためには、実装するメンバを Private で修飾します。

Module Module1

    Sub Main()
        Dim one = New Getter()
        Dim ione = DirectCast(one, IGetOne)

        ' コンパイルエラー
        Console.WriteLine(one.GetOne())

        ' OK
        Console.WriteLine(ione.GetOne())
    End Sub

End Module

Interface IGetOne
    Function GetOne() As Integer
End Interface

Class Getter
    Implements IGetOne

    ' Private にすると明示的実装
    Private Function GetOne() As Integer Implements IGetOne.GetOne
        Return 1
    End Function
End Class

VB ならではの機能?

上記では明示的実装のために Private を使いましたが、Protected や Friend を指定することもできます。つまり、次のようなこともできます。

Module Module1

    Sub Main()
        Dim one = New Getter()
        Dim ione = DirectCast(one, IGetOne)

        ' コンパイルエラー
        Console.WriteLine(one.GetOne())

        ' OK
        Console.WriteLine(ione.GetOne())
    End Sub

End Module

Interface IGetOne
    Function GetOne() As Integer
End Interface

Class Getter
    Implements IGetOne

    ' Protected で明示的実装することもできる
    Protected Function GetOne() As Integer Implements IGetOne.GetOne
        Return 1
    End Function
End Class

Class Getter2
    Inherits Getter

    Sub action()
        ' OK
        Dim one = MyBase.GetOne()
    End Sub
End Class

この例は Protected ですが、正直あまり使いどころがわかりません。ただ、Friend だとそれなりに便利かも、というかフレームワークやライブラリで呼び出すときだけインターフェイスにキャストしなくても良いようにする、という目的だとむしろ Private を使うよりも適切かもしれません。

ちなみにちょっと試した限りでは、C# では明示的実装を行うとそのメンバは常に private 相当になり、protected や internal 相当で明示的実装をすることはできないっぽいので、VB ならではの機能、ということになります。

Node.js の開発環境構築(Debian 編)

このところ業務で Node.js をやる可能性がほんのり出てきたので、開発環境の構築方法をメモしてみます。よくわかってないでとりあえずやってみてる感じなので、実用的かどうかはわかりませんが。

nvm(Node Version Manager) のインストール

Debian だったら何でも apt-get で入れたくなってしまいますが、Debian では node というパッケージ名およびコマンド名が昔から別のパッケージに使われているせいで、Node.js のものはどちらも nodejs というものに変更されており、色々面倒なんだとか。
それに Node.js はバージョンがいろいろあるので、独自のバージョン管理システムを使い、インストールおよびバージョンを随時切り替えて使用するのが一般的らしいです。
中でも nvm(Node Version Manager) というのがメジャーっぽいのでこれを使うことにします。これは各ユーザのホームディレクトリにインストールするので、root 権限はいらないし環境も汚れないのが特徴みたいです。まさに開発環境向けなんですかね。

以下概ね 公式サイト の記述に従ってインストールしていきます。

インストールは bashスクリプトをダウンロードしてきて、それを実行することで行うようです。cURL または Wget を使う方法が挙げられていますが、とりあえず cURL を使うことにします。もし入ってなければ、こちらは apt-get でインストールします。

$ sudo apt-get install curl

次の手順でダウンロードするスクリプトを見てみると、何だか Git を使ってるみたいです。別になくても別の方法でインストールしてくれるみたいですが、ここは素直に Git で行きましょう。というわけで万一入ってなければこれも apt-get。

$ sudo apt-get install git

以上で準備できましたので、cURLスクリプトをダウンロードして実行します。

$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.4/install.sh | bash

出力はこんな感じ。ところどころにある nodeuser というのは、スクリプトを実行したユーザ名です。

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  9135  100  9135    0     0  55274      0 --:--:-- --:--:-- --:--:-- 55701
=> Downloading nvm from git to '/home/nodeuser/.nvm'
=> Cloning into '/home/nodeuser/.nvm'...
remote: Counting objects: 5191, done.
remote: Compressing objects: 100% (27/27), done.
remote: Total 5191 (delta 16), reused 0 (delta 0), pack-reused 5164
Receiving objects: 100% (5191/5191), 1.39 MiB | 450.00 KiB/s, done.
Resolving deltas: 100% (3124/3124), done.
Checking connectivity... done.
* (detached from v0.31.4)
  master

=> Appending source string to /home/nodeuser/.bashrc
=> Close and reopen your terminal to start using nvm or run the following to use it now:

export NVM_DIR="/home/nodeuser/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"  # This loads nvm

.bashrc に必要な設定をしたのでターミナルを開き直すか、最後の 2 行のコマンドを実行すれば nvm が使えるよ!と言われているので、ここは開き直しで。

nvm を使った Node.js のインストール

nvm は複数バージョンの Node.js 環境を切り替えられるので、Node.js インストールの際もバージョンを指定します。以下のコマンドで現在インストール可能なバージョン一覧を出力します。

$ nvm ls-remote

省略しますが、山のように表示されてしまいました。一旦これはナシで。
先ほどのコマンドにはさらに追加のオプションがあって、LTS(long-term support)版、つまりある程度長期的に安心して使えるバージョンのものだけを表示することもできるようです。

$ nvm ls-remote --lts
         v4.2.0   (LTS: Argon)
         v4.2.1   (LTS: Argon)
         v4.2.2   (LTS: Argon)
         v4.2.3   (LTS: Argon)
         v4.2.4   (LTS: Argon)
         v4.2.5   (LTS: Argon)
         v4.2.6   (LTS: Argon)
         v4.3.0   (LTS: Argon)
         v4.3.1   (LTS: Argon)
         v4.3.2   (LTS: Argon)
         v4.4.0   (LTS: Argon)
         v4.4.1   (LTS: Argon)
         v4.4.2   (LTS: Argon)
         v4.4.3   (LTS: Argon)
         v4.4.4   (LTS: Argon)
         v4.4.5   (LTS: Argon)
         v4.4.6   (LTS: Argon)
         v4.4.7   (LTS: Argon)
         v4.5.0   (Latest LTS: Argon)

v4.5.0 が LTS 版では一番新しいっぽいですね。というわけでこいつをインストール...したいところですが、そもそも何で Node.js 環境を用意しようとしてるかというと、実は AWS Lambda で使おうとしてるからだったりします。AWS のマニュアル によると、Lambda の Node.js 対応バージョンは v0.10.36 と v4.3.2 ということなので(リンク先は英語ですが、日本語マニュアルだと v0.10.36 しか書かれていません)、今回は v4.3.2 をインストールします。

$ nvm install v4.3.2
Downloading https://nodejs.org/dist/v4.3.2/node-v4.3.2-linux-x64.tar.xz...
######################################################################## 100.0%
Now using node v4.3.2 (npm v2.14.12)
Creating default alias: default -> v4.3.2

なんか npm とかいうのが見えますが、これは JavaScript のパッケージマネージャで、Node.js をインストールすると一緒についてくるみたいです。Node.js 専用のものかはまだよく調べてないのでワカリマセン。

Visual Studio Code のインストール

開発環境というからにはエディタも必要。Linux だったらエディタは Vim を嗜むべきなのかもしれませんが、私は Vim なんてさっぱりでスクリーンエディタは nano しか使えないクソ雑魚ですので、もっと甘やかしてくれるエディタがいいです。
まぁ MS 系プログラマーとしてはやっぱり Visual Studio Code ですよね。Debian 向けに deb パッケージもちゃんと用意されてるのがありがたい。
これも cURL でゲットできるとスマートな感じですが、go.microsoft.com によるリダイレクトをうまく扱う方法がわからないので、素直にブラウザで公式サイト から deb パッケージをダウンロードしてきます。

ダウンロードできたら dpkg でインストール。アップデートの場合も同じコマンドで OK です。

$ sudo dpkg -i code_1.4.0-1470329130_amd64.deb

使用しているデスクトップ環境にもよりますが、インストールするとちゃんとスタートメニュー的なところに追加してくれるみたいです。これは LXDE の場合です。

f:id:s_earlgrey:20160821165715p:plain

もしくはターミナルから code コマンドでも起動できます。よくこのコマンド名空いてたな...。

$ code

Windows における Node.js 環境構築のガイドライン によると、Visual Studio Code では eslint/npm script runner/EditorConfig の 3 つの拡張機能がオススメされていますので、これもインストールしておきます。

f:id:s_earlgrey:20160821171300p:plainf:id:s_earlgrey:20160821171307p:plainf:id:s_earlgrey:20160821171500p:plain

最初はとりあえずこんなところでしょうか。実際に何か作ってみるといろいろ足りないものもわかってくるかもしれませんが、それはまぁぼちぼち。
(というか今兼務してる他案件でドはまり中で、Node.js どころではなかったりするのですが...。)

MDF ファイルのみからデータベースをリストアする方法

先日開発環境でやらかしたので、忘れないうちに自分用にメモします。

注意とお約束

これから記述する内容は無保証ですので、適用する場合は自己責任でお願いします。
また、最終手段としてとりあえずデータベースを認識させるためのものであり、障害発生時点の状態に戻せるわけではなく、データページに破損がある場合や、チェックポイント未実行のデータがある場合、それらは失われます。また、MDF ファイルが著しく破損している場合はこの方法でも認識できないことがあるかもしれません。

前提

アタッチ機能で MDF ファイルからデータベースをリストアする場合、普通は LDF ファイル(トランザクションログ)を一緒に指定します。
ただし、もともと LDF ファイルの内容がすべて MDF に適用されていた状態であれば、MDF 単独でもアタッチ可能です。この場合、アタッチ後には初期化された LDF ファイルが自動的に作成されます。

Management Studio の GUI では、「データベースの詳細」グリッドから LDF ファイルを選択して削除します。

f:id:s_earlgrey:20160515220129p:plain

ただし、LDF ファイルに未適用データが存在していた場合は、この方法ではアタッチできません。強行しようとしても、以下のようなダイアログが表示されてしまいます。

f:id:s_earlgrey:20160515194904p:plain

指示に従って詳細を確認すると、次のように表示されます。

f:id:s_earlgrey:20160515195132p:plain

LDF ファイルが正しくないと書いてありますね。でも LDF ファイルがない場合はしょうがないので、多少のデータ消失リスクは受け入れて、なんとかデータベースをリストアして生きてるデータだけでも救出したいところです。

リストア手順

それではこれから具体的な手順を書いていきます。

まずは、復旧したいデータベースと同じ名前で新しいデータベースを作成します。今回の例では「Northwind」です。
作成したら、SQL Server のサービスを一旦停止します。その後で、リストアしたい MDF ファイルを、先ほど作成したデータベースの MDF ファイルに上書きコピーして、SQL Server のサービスを起動します。

何をしているかというと、まずは何とかして SQL Server に MDF ファイルをロードしてもらう必要があるので、一旦破損していないデータベースを作っておいて、その MDF ファイルを差し替えるということをしています。

当然、これだけでリストアできるほど単純ではありません。起動後、Management Studio で接続すると、オブジェクトエクスプローラーには次のように「(復旧待ち)」と表示され、テーブル等を参照することはできません。

f:id:s_earlgrey:20160515202145p:plain

続いて以下の SQL を実行してデータベースを復旧します。Northwind のところは実際のデータベース名に読み替えてください。

USE master
GO

-- データベースを緊急モードに切り替える
ALTER DATABASE Northwind SET EMERGENCY
GO

-- データベースをシングルユーザモードに切り替える
ALTER DATABASE Northwind SET SINGLE_USER
GO

-- データベースを復旧する
DBCC CHECKDB ('Northwind',REPAIR_ALLOW_DATA_LOSS)
GO

-- データベースをオンラインに切り替える(省略可)
ALTER DATABASE Northwind SET ONLINE
GO

-- データベースをマルチユーザモードに切り替える
ALTER DATABASE Northwind SET MULTI_USER
GO

なお、緊急モードへ切り替えるのに次のような SQL を紹介しているサイトもあります。これは SQL Server 2000 まではこの方法を使っていたようですが、2005 以降では最後の UPDATE で「システム カタログへのアドホック更新は許可されません。」というエラーになります。
2005 以降は上記の方法で OK です。

sp_configure 'allow updates', 1
GO

RECONFIGURE
GO

UPDATE sys.sysdatabases SET STATUS = 32768 WHERE dbid = db_id('Northwind')
GO

DBCC CHECKDB は、MDF ファイルが大きいほど時間がかかりますのでしばらくお待ちください。完了すると以下のようなメッセージが表示されます。

メッセージ 5173、レベル 16、状態 1、行 12
データベースのプライマリ ファイルと一致しないファイルが 1 つ以上あります。データベースをアタッチする場合は、正しいファイルを使用して操作を再試行してください。これが既存のデータベースの場合は、ファイルが壊れている可能性があるので、バックアップから復元する必要があります。
ログ ファイル 'C:\Program Files\Microsoft SQL Server\MSSQL12.SQLEXPRESS\MSSQL\DATA\northwind_log.ldf' がプライマリ ファイルと一致しません。このログ ファイルは別のデータベースまたはログから以前に再構築された可能性があります。
警告: データベース 'Northwind' のログが再構築されました。トランザクションの一貫性は失われました。RESTORE チェーンが壊れ、サーバーが以前のログ ファイルのコンテキストを保持しなくなったので、以前のログ ファイルについて把握しておく必要があります。DBCC CHECKDB を実行して物理的な一貫性を検証してください。データベースは dbo 専用モードに設定されました。データベースが使用可能な状態になったら、データベース オプションを再設定し、余分なログ ファイルを削除してください。
'Northwind' の DBCC 結果。
(省略)
CHECKDB により、データベース 'Northwind' に 0 個のアロケーション エラーと 0 個の一貫性エラーが見つかりました。
DBCC の実行が完了しました。DBCC がエラー メッセージを出力した場合は、システム管理者に相談してください。

かなり長々と表示されますが、大事なのは最後から 2 行目の内容でしょうか。とりあえず、 MDF ファイルに破損はなかったと書かれています。

ただし、エラーがなかったからと言って、データがすべて復旧したわけではありません。最初にも書きましたが、チェックポイント未実行のデータがあった場合、それらは失われます。
実際に上記のメッセージは、チェックポイント未実施で SQL Server を強制終了した時の MDF ファイルからリストアした時のものですが、データの内容は最後のチェックポイント時点まで戻っていました。

VB レガシー機能の多くを封じる方法

今回は、2 年前にとある勉強会で発表した時のネタの一部ですが、今でもこれについて具体的に紹介した日本語のリソースが見当たらないので投稿することにしました。

VB6 おじさん

昨今良くネタにされる、SIer を始めとした労働集約型の開発チームだと、政治的理由でプラットフォームに VB が選択されることも良くあると思います。VB 自体はそこまで悪い言語ではないですが(ただし個人的にはやはり機能面で C# を推したいです)、そういうチームで往々にして問題になるのが、VB6 時代の知識で止まっている人たちの存在です。
私だったら VB であることはやむなしとして、なるべく最新の言語仕様による記述を心掛けたいと思うところですが、VB6 おじさん(30 歳ちょいでも結構いたりする...)はそんなのお構いなく VB6 風味のコードを量産してくれます。

IDEコンパイラレベルでの対策

Option Strict On オプションによる、型付の厳格化/遅延バインディングの抑止はわりと多くのプロジェクトで採用されているかと思いますが、これだけでもそれなりに効果的です。でもできればもうひと押し欲しい!!

Visual Studio 2010 SP1 以降が必要ですが、そんな状況では、プロジェクトファイル(.vbproj)を直接テキストエディタで開き、どこかの PropertyGroup 要素の配下に "<VBRuntime>Embed</VBRuntime>" を追加します。私はよく次の画像の位置に書いてます。

f:id:s_earlgrey:20160509014823p:plain

これを行うことで、そのプロジェクトではいくつかの VB 固有機能(多くは VB6 風味)を無効化することができます。主には次のようなものです。

次のような機能は依然として有効です。

  • モジュール
    • VB では拡張メソッドはモジュールに定義する必要があるので...。
  • 暗黙の縮小変換
    • Double から Integer への変換とか
  • Microsoft.VisualBasic.CompilerServices.Conversions クラス
  • AscW/ChrW メソッド
  • vbBack/vbCr/vbCrLf/vbFormFeed/vbLf/vbNewLine/vbNullChar/vbNullString/vbTab/vbVerticalTab 定数
  • XML リテラル
  • Function と同名の暗黙のローカル変数
  • With
  • プロパティの参照渡し
  • Nullable との比較における三値論理

というわけで、あまりうれしくない機能も残りますが(個人的に下から 4 つは心底忌み嫌っています)、わりと強力です。特に既定のインスタンスが無効化されるのはうれしいかもしれません。

注意として、CInt 等の変換関数の機能にも変化が表れます。例えば CInt("123") のような全角数字は、通常なら 123 に変換してくれますが、<VBRuntime>Embed</VBRuntime> を設定している場合は InvalidCastException になります。実行してみないと違いがわからないところが少し厄介かもしれません。
このあたりは Culture を参照するかどうかが変わってくるように感じていますが、あまり調べられていません。

現実問題

ではこの機能を使って VB6 風味のコードを積極的に制限すべきかというと、実はそうは思っていません。制限される機能は VB の持ち味でもあり、それらがないものはもはや C# の劣化版に過ぎないからです。品質に小うるさい案件では厳格なコードが必要とは思いますが、だったら素直に C# を使ったほうが良いかと。
メンバーの反発も予想されます。

というわけで、私も実際に適用したのは、限られたメンバーだけが編集するコアなライブラリのプロジェクトでだけです。

おまけ

ちなみに冒頭で触れた勉強会の発表資料はこちらです。2 年前のものですが特に加筆はしていません。

www.slideshare.net

過去に登壇したのはこの一度きりですが、また何かネタを見つけてチャレンジしたいですね。

sp_executesql におけるオブジェクト名の記述方法

SqlClient でパラメータを使用した SQL を実行すると、実際は sp_executesql に変換されます。
プロファイラを使えばその様子を簡単に観察することができます。

using System;
using System.Data;
using System.Data.SqlClient;

static void Main(string[] args)
{
    using (var conn = new SqlConnection(@"Data Source=localhost\SQLEXPRESS;Initial Catalog=AdventureWorksDW2014;Integrated Security=True"))
    using (var paramCmd = new SqlCommand("SELECT [ProductAlternateKey] FROM [DimProduct] WHERE [ProductKey] = @ProductKey", conn))
    using (var adhocCmd = new SqlCommand("SELECT [ProductAlternateKey] FROM [DimProduct] WHERE [ProductKey] = 1", conn))
    {
        conn.Open();

        // パラメータクエリの実行
        var param = new SqlParameter("ProductKey", SqlDbType.Int);
        paramCmd.Parameters.Add(param);
        param.Value = 1;
        var productAlternateKey = paramCmd.ExecuteScalar() as string;
        Console.WriteLine(productAlternateKey);

        // アドホッククエリの実行
        productAlternateKey = adhocCmd.ExecuteScalar() as string;
        Console.WriteLine(productAlternateKey);
    }

    Console.ReadKey();
}

上記のコードを実行したときのプロファイラの出力結果がこちらです。

f:id:s_earlgrey:20160505192832p:plain

色の付いた行がパラメータクエリで、確かに sp_executesql が実行されていることがわかります。一方で、後続のアドホッククエリは SQL がそのまま実行されています。

ちなみにデータベースにはデータウェアハウス用の Adventureworks を使用しています。(Adventure Works DW 2014 Full Database Backup.zip)

さてその sp_executesql ですが、リファレンスにこのような記述があります。

パフォーマンスを向上させるには、ステートメント文字列に完全修飾オブジェクト名を使用します。

完全修飾オブジェクト名というのは、サーバ名.データベース名.スキーマ名.オブジェクト名で表されます。つまりそのまま解釈すると、先のコードの場合だと、[APOLLO\SQLEXPRESS].[AdventureWorksDW2014].[dbo].[DimProduct] と書く必要があるということです(APOLLO というのはコンピュータ名です)。
これはめんどくさい。特にサーバ名は localhost じゃダメらしいので、開発環境と本番環境のサーバ名の違いを吸収するのが大変です。

一体なぜこんなことを?という感じですが、別の説明ページ にはこういう風に書かれています。

SQL Server によって実行プランが再利用されるようにするには、ステートメント文字列内のオブジェクト名を完全修飾名にする必要があります。

えええ本当ですか...、という感じです。正直信じがたいので、完全修飾名じゃない場合に実行プランが再利用されるかどうかを確かめてみます。

完全修飾でないどころか、データベース名やスキーマ名も省略したオブジェクト名を使って、同じ sp_executesql をパラメータ値を変えて 2 回実行します。

-- キャッシュ済実行プランをクリア
DBCC FREEPROCCACHE
GO

EXEC sp_executesql
    N'SELECT [ProductAlternateKey] FROM [DimProduct] WHERE [ProductKey] = @ProductKey',N'@ProductKey int',
    @ProductKey = 1
GO

EXEC sp_executesql
    N'SELECT [ProductAlternateKey] FROM [DimProduct] WHERE [ProductKey] = @ProductKey',N'@ProductKey int',
    @ProductKey = 2
GO

これを実行した後で、キャッシュされた実行プランを確認してみます。

SELECT usecounts, cacheobjtype, objtype, text 
FROM sys.dm_exec_cached_plans 
    CROSS APPLY sys.dm_exec_sql_text(plan_handle) 
GO

f:id:s_earlgrey:20160505205315p:plain

sys.dm_exec_cached_plans
sys.dm_exec_sql_text

これを見る限り、2 回とも同じ実行プランが利用されています。少なくとも実行プラン再利用の観点からすると、sp_executesql で完全修飾オブジェクト名を使う必要はなさそうです。