Windows と C++

Windows Azure 上のデータベースを使用する

Kenny Kerr

Kenny Kerrマイクロソフトは長年、多数のデータ アクセス テクノロジで開発者の混乱を招いてきました。Windows、SQL Server、または Visual Studio をリリースするたびに新しいデータ アクセス API を導入しているように思えた時期もありました。その歴史の中で (確か 1996 年ごろでした)、マイクロソフトはいつもの情熱から、開発者に ODBC から OLE DB への移行を促しました。

ODBC は Open Database Connectivity の略で、データベース管理システムにアクセスする従来の標準でした。OLE DB は (思い出すのにちょっと時間がかかりますが) Object Linking and Embedding Database の略で、新しい汎用データベース アクセスの理想的な手法でした。しかし、この名前は完全に誤解されやすい名前でした。これについては後で説明します。

1999 年 7 月号の MSDN マガジン (当時の誌名は『Microsoft Systems Journal』) で Don Box のコラムを読んだことは、今でも思い出せます。そのコラムでは、OLE DB に移行する動機と発想について説明していました。そのとき自分が、このテクノロジは ODBC よりもはるかに複雑だけれども、間違いなくはるかに拡張性が高いと考えていたことを覚えています。OLE DB という名前が非常に誤解されやすい理由は、OLE DB が OLE とまったく関係がないうえに、データベース専用のテクノロジでもないことにあります。実のところ OLE DB は、テキスト、XML、データベース、検索エンジンなど、リレーショナルかどうかにかかわらず、すべてのデータを対象とする汎用データ アクセス モデルとして設計されました。OLE DB が登場した当時は Windows プラットフォームで COM が大流行していたため、OLE DB の COM を多用した API とその生来の拡張性は、多くの開発者の心を捕らえました。

しかし、リレーショナル データベース API としては、OLE DB は ODBC の優れたパフォーマンスを実現することはできませんでした。その後のデータ アクセス テクノロジ (Microsoft .NET Framework のデータ アクセス テクノロジなど) には、データベース アクセス機能だけが欠けていたため、汎用データ アクセスという夢はかすみ始めました。そして 2011 年 8 月、OLE DB の最大の支持者だった SQL Server チームは、bit.ly/1dsYgTD (英語) で「Microsoft is aligning with ODBC for native relational data access」(マイクロソフトはネイティブ リレーショナル データベース アクセスに関して ODBC と連携します) という驚くべき記事を発表しました。SQL Server チームは、市場が OLE DB から ODBC に向かっていると述べました。したがって、おわかりのとおり、次世代のネイティブ C++ アプリケーション向けの SQL Server にアクセスする方法は、ODBC に戻りました。

長所を挙げれば、ODBC は比較的単純です。しかも、非常に高速です。OLE DB のパフォーマンスが ODBC を上回ったという主張はよくありましたが、実際に上回ることはめったにありませんでした。短所を挙げれば、ODBC は、使い方を覚えている (またはそもそも学んだことがある) 開発者がほとんどいない、従来の C スタイルの API です。さいわい、救済策として、最新の C++ を使用すれば ODBC のプログラミングが簡単になります。C++ で Windows Azure 上のデータベースにアクセスするには、ODBC を採用する必要があります。では、説明しましょう。

他のさまざまな C スタイルの API と同様に、ODBC のモデルは、オブジェクトを表す一連のハンドルに基づいています。そこで今回も、多数のコラムで使用してきた自作の頼もしい unique_handle クラス テンプレートを使用することにします。handle.h については、dx.codeplex.com (英語) でコピーを入手して内容を調べることができます。ただし、ODBC のハンドルはあまり使いやすくありません。ODBC API には、ハンドル (およびその基盤となるオブジェクト) の作成時にも解放時にも、使用時に各ハンドルの型を指定する必要があります。

ハンドルの型は SQLSMALLINT で表しますが、これは単なる短整数型です。ODBC で定義されているオブジェクトの型ごとに unique_handle の特徴 (traits) クラスを定義する代わりに、今回は特徴クラス自体をテンプレートにしましょう。図 1 に、特徴クラスの例を示します。

図 1 ODBC の特徴クラス

 

template <SQLSMALLINT T>
struct sql_traits
{
  using pointer = SQLHANDLE;
  static auto invalid() noexcept -> pointer
  {
    return nullptr;
  }
  static auto close(pointer value) noexcept -> void
  {
    VERIFY_(SQL_SUCCESS, SQLFreeHandle(T, value));
  }
};

図 1 に示す特徴クラスの close メソッドをご覧になると、ODBC のジェネリック関数とハンドルを併用する際は ODBC に各ハンドルの型を指定する必要があることがわかります。今回は Visual C++ コンパイラの最新プレビュー ビルド (この記事の執筆時点では November 2013 CTP) を使用しているので、重複して指定している例外のスローを noexcept 指定子に置き換えることができます。noexcept 指定子を使用すると、コンパイラで生成されるコードを向上できる場合があります。残念ながら、このコンパイラには auto 関数の戻り値を推測する機能もありますが、バグのためクラス テンプレートのメンバー関数については推測できません。もちろん、特徴クラス自体はクラス テンプレートなので、テンプレートのエイリアスが役に立ちます。

 

template <SQLSMALLINT T>
using sql_handle = unique_handle<sql_traits<T>>;

これで、さまざまな ODBC オブジェクトの型のエイリアスを定義できるようになりました。たとえば、環境オブジェクトとステートメント オブジェクトの場合は、次のようになります。

using environment = sql_handle<SQL_HANDLE_ENV>;
using statement = sql_handle<SQL_HANDLE_STMT>;

接続オブジェクトにもエイリアスを定義しますが、もっと具体的な名前を使用します。

using connection_handle = sql_handle<SQL_HANDLE_DBC>;

このような名前を付ける理由は、接続を必ずクリーンアップするには追加の処理が少し必要なためです。環境オブジェクトとステートメント オブジェクトに必要な処理も大差ありませんが、接続オブジェクトには、接続状態に確実に対処するためにどうしても connection クラスが必要になります。接続に対処するには、環境を作成する必要があります。

SQLAllocHandle ジェネリック関数は、さまざまなオブジェクトを作成します。以下に示すとおり、この関数についてもオブジェクト (または少なくともハンドル) とその型を分離しています。このコードを何回も繰り返す代わりに、ここでも関数テンプレートを使用して、型情報を元どおりにまとめます。ODBC の SQLAllocHandle ジェネリック関数の関数テンプレートは、次のとおりです。

template <typename T>
auto sql_allocate_handle(SQLSMALLINT const type,
                         SQLHANDLE input)
{
  auto h = T {};
  auto const r = SQLAllocHandle(type,
                                input,
                                h.get_address_of());
  // TODO: check result here ...
  return h;
}

もちろん、この関数テンプレートも ODBC 関数と同様にジェネリックですが、C++ で扱いやすい方法で、ジェネリックな性質を公開しています。エラー処理については、後で説明します。この関数テンプレートは指定した型のハンドルを割り当ててハンドル ラッパーを返すので、先ほど定義した型のエイリアスを使用できます。環境の場合は、次のように記述できます。

auto e = sql_allocate_handle<environment>(SQL_HANDLE_ENV, nullptr);

入力ハンドルや親ハンドルと呼ばれる 2 番目のパラメーターは、省略可能な親ハンドルを一部の論理コンテインメントに提供します。環境には親がありませんが、接続オブジェクトの親として機能します。残念ながら、環境を作成するにはもう少し手順が必要です。ODBC に、予期している ODBC のバージョンを指定する必要があります。そのためには、SQLSetEnvAttr 関数で環境属性を設定します。便利なヘルパー関数でラップすると、次のようになります。

auto create_environment()
{
  auto e = 
    sql_allocate_handle<environment>(SQL_HANDLE_ENV, nullptr);
  auto const r = SQLSetEnvAttr(e.get(),
    SQL_ATTR_ODBC_VERSION,
    reinterpret_cast<SQLPOINTER>(SQL_OV_ODBC3_80),
    SQL_INTEGER);
  // TODO: check result here ...
  return e;
}

これで、接続を作成できるようになりました。さいわい、接続の作成は非常に簡単です。

auto create_connection(environment const & e)
{
  return sql_allocate_handle<connection_handle>(
    SQL_HANDLE_DBC, e.get());
}

接続は環境のコンテキストで作成されます。ご覧のとおり、環境を接続の親として使用しています。ただし、実際の接続を作成する必要があります。この処理は SQLDriverConnect 関数で実行しますが、一部のパラメーターは無視できます。

 

auto connect(connection_handle const & c,
             wchar_t const * connection_string)
{
  auto const r = SQLDriverConnect(c.get(), nullptr,
    const_cast<wchar_t *>(connection_string),
    SQL_NTS, nullptr, 0, nullptr,
    SQL_DRIVER_NOPROMPT);
  // TODO: check result here ...
}

特に SQL_NTS 定数は、直前の接続文字列が null で終了していることを関数に通知しているだけです。定数の代わりに、長さを明示的に指定することもできます。最後の SQL_DRIVER_NOPROMPT 定数は、接続を確立するために追加情報が必要な場合に、ユーザーにプロンプトを表示するかどうかを示します。ここでは、プロンプトを無効にしています。

しかし、先ほど触れたように、接続を適切に閉じるには追加の処理が少し必要です。問題は、接続ハンドルを解放するには SQLFreeHandle 関数を使用しますが、この関数は接続が閉じていることを前提にしており、接続が開いていても自動的には閉じません。

そのため、接続の接続状態を追跡する connection クラスが必要になります。これは、次のようになります。

class connection
{
  connection_handle m_handle;
  bool m_connected { false };
public:
  connection(environment const & e) :
    m_handle { create_connection(e) }
  {}
  connection(connection &&) = default;
  // ...
};

これで、先ほど定義した connect 非メンバー関数を使用してクラスに connect メソッドを追加し、接続状態を更新できるようになりました。

auto connect(wchar_t const * connection_string)
{
  ASSERT(!m_connected);
  ::connect(m_handle, connection_string);
  m_connected = true;
}

connect メソッドは、接続が開いていないことを最初にアサートし、最後に接続が開いていることを追跡します。このようにすると、connection クラスのデストラクターで、必要に応じて自動的に接続を終了できます。

~connection()
{
  if (m_connected)
  {
    VERIFY_(SQL_SUCCESS, SQLDisconnect(m_handle.get()));
  }
}

この結果、メンバー ハンドル デストラクターを呼び出して接続ハンドル自体を解放する前に、接続が終了するようになります。ここまで完了すれば、わずか数行のコードで、適切かつ効率的な方法で ODBC 環境を作成して接続を確立できます。

auto main()
{
  auto e = create_environment();
  auto c = connection { e };
  c.connect(L"Driver=SQL Server Native Client 11.0;Server=...");
}

ステートメントについてはどうでしょうか。ステートメントの場合も sql_allocate_handle 関数テンプレートが役に立ちます。ここでは、connection クラスに新しいメソッドを追加します。

auto create_statement()
{
  return sql_allocate_handle<statement>(SQL_HANDLE_STMT,
                                        m_handle.get());
}

ステートメントは接続のコンテキストで作成されます。ご覧のとおり、接続はステートメント オブジェクトの親です。先ほどの main 関数で、ごく簡単にステートメント オブジェクトを作成できます。

auto s = c.create_statement();

ODBC には SQL ステートメントを実行するためのかなり単純な関数が用意されていますが、利便性のためにこの関数についてもラップします。

auto execute(statement const & s,
             wchar_t const * text)
{
  auto const r = SQLExecDirect(s.get(),
                               const_cast<wchar_t *>(text),
                               SQL_NTS);
  // TODO: check result here ...
}

ODBC はきわめて古風な C スタイルの API なので、const を使用しません。C++ コンパイラ向けに条件付きで使用することさえありません。今回の例では、"const らしさ" を取り除いて、このような const への非対応から呼び出し元を保護しています。main 関数で SQL ステートメントを作成するのは実に簡単です。

execute(s, L"create table Hens ( ... )");

しかし、結果セットを返す SQL ステートメントを実行するとしたらどうでしょうか。次のようなステートメントを実行するとしたらどうなるでしょう。

execute(s, L"select Name from Hens where Id = 123");

この場合、ステートメントは実質的にカーソルになり、結果があれば 1 つずつ取得する必要があります。これは SQLFetch 関数の役割です。たとえば、特定の ID のメンドリ (hen) が存在するかどうか特定する場合が考えられます。

if (SQL_SUCCESS == SQLFetch(s.get()))
{
  // ...
}

一方、複数の行を返す SQL ステートメントを実行する場合も考えられます。

execute(s, L"select Id, Name from Hens order by Id desc");

その場合は、SQLFetch 関数をループ内で呼び出すだけです。

while (SQL_SUCCESS == SQLFetch(s.get()))
{
  // ...
}

列の値を個別に取得する処理は、SQLGetData 関数の役割です。SQLGetData 関数もジェネリック関数なので、予期する情報に加えて、結果の値のコピー先バッファーも正確に指定する必要があります。固定サイズの値を取得する処理は比較的簡単です。図 2 に、SQL 整数値を取得する単純な関数を示します。

図 2 SQL 整数値の取得

auto get_int(statement const & s,
             short const column)
{
  auto value = int {};
  auto const r = SQLGetData(s.get(),
                            column,
                            SQL_C_SLONG,
                            &value,
                            0,
                            nullptr);
  // TODO: check result here ...
  return value;
}

SQLGetData 関数の最初のパラメーターは、ステートメント ハンドルです。2 番目のパラメーターは 1 から始まるインデックスで、3 番目のパラメーターは SQL 整数型に対応する ODBC の型、4 番目のパラメーターは、値を受け取るバッファーのアドレスです。今回取得する値は固定サイズのデータ型なので、5 番目のパラメーターは無視します。他のデータ型の場合、このパラメーターは入力時のバッファー サイズを指定します。最後のパラメーターは、バッファーにコピーするデータの実際の長さまたはサイズを指定します。このパラメーターも固定サイズのデータ型には使用しませんが、値が null だったかどうかなどの状態情報を返すために使用することもできます。文字列値を取得する処理は、もう少しだけ複雑になります。図 3 に、値をローカル配列にコピーするクラス テンプレートを示します。

図 3 SQL 文字列値の取得

template <unsigned Count>
auto get_string(statement const & s,
                short const column,
                wchar_t (&value)[Count])
{
  auto const r = SQLGetData(s.get(),
                            column,
                            SQL_C_WCHAR,
                            value,
                            Count * sizeof(wchar_t),
                            nullptr);
  sql_check(r, SQL_HANDLE_STMT, s.get());
}

この処理では値を受け取るバッファーの実際のサイズを SQLGetData 関数に指定する必要があることと、サイズを文字単位ではなくバイト単位で指定する必要があることに注目してください。特定のメンドリの名前をクエリした場合に、Name 列に最大で 100 文字が含まれているときは、次のような get_string 関数を使用できます。

if (SQL_SUCCESS == SQLFetch(s.get()))
{
  wchar_t name[101];
  get_string(s, 1, name);
  TRACE(L"Hen’s name is %s\n", name);
}

最後に、接続オブジェクトを再利用して複数のステートメントを実行できますが、ステートメント オブジェクトがカーソルを表す場合は、次のステートメントを実行する前に必ずカーソルを閉じる必要があります。

VERIFY_(SQL_SUCCESS, SQLCloseCursor(s.get()));

皮肉なことに、これはリソース管理で問題になりません。開いている接続の問題とは異なり、ステートメントに開いているカーソルがあっても SQLFreeHandle 関数は影響を受けません。

ここまでは、エラー処理についての説明を省略してきました。これは、エラー処理がそもそも複雑な処理だからです。ODBC 関数はエラー コードを返すので、開発者がこのようなリターン コードの値をチェックして、操作が成功したかどうか特定する必要があります。関数は、成功を表す SQL_SUCCESS 定数を返すことがほとんどですが、SQL_SUCCESS_WITH_INFO 定数を返すこともあります。SQL_SUCCESS_WITH_INFO 定数も成功を表しますが、詳細な診断情報を必要に応じて取得できることも表します。一般に、私はデバッグ ビルド時にのみ、SQL_SUCCESS_WITH_INFO 定数が返されたら診断情報を取得します。このようにすると、開発時にできる限り多くの情報を収集でき、運用時のサイクルが無駄になりません。もちろん、エラー コードが本当に返された場合は必ず診断情報を収集しています。もちろん、エラー コードが本当に返された場合は必ず診断情報を収集しています。

ODBC では診断情報が結果セットとして提供されるので、SQLGetDiagRec 関数と 1 から始まる行インデックスを使用して、1 行ずつ取得できます。念のため、関数を呼び出す際は、問題のエラー コードが発生したオブジェクトのハンドルを渡してください。

各行には、主に 3 つの情報が含まれています。具体的には、ODBC データ ソースまたはドライバーに固有のネイティブ エラー コード、レコードが表しているエラーのクラスを定義する短い暗号のような 5文字の状態コード、および診断レコードをより長いテキスト形式で表した説明です。必要なバッファーを確保すれば、ループ内で SQLGetDiagRec 関数を呼び出すだけで、すべての情報を取得できます (図 4 参照)。

図 4 診断エラー情報の取得

 

auto native_error = long {};
wchar_t state[6];
wchar_t message[1024];
auto record = short {};
while (SQL_SUCCESS == SQLGetDiagRec(type,
                                    handle,
                                    ++record,
                                    state,
                                    &native_error,
                                    message,
                                    _countof(message),
                                    nullptr))
{
  // ...
}

Windows Azure を SQL Server と併用すると、ホストされているデータベースの開発を驚くほど簡単に始めることができます。SQL Server のデータベース エンジンが、C++ 開発者が何年も前から知っていて使用してきたエンジンと同じ場合は、特に効果が高くなります。OLE DB は使用されなくなりましたが、ODBC の方が処理に適しており、OLE DB では実現できたことがないほど簡単で高速です。もちろん、その威力をわかりやすい方法で活かすには、C++ の力を少し借りる必要があります。

Visual C++ を使用して Windows Azure 上のデータベースにアクセスする方法の詳細については、私が作成した Pluralsight のコース「Visual C++ を強化する実践的な 10 の手法」(bit.ly/1fgTifi、英語) を参照してください。このコースには、データベース サーバーをセットアップおよび使用し、列をバインドして複数行データの取得処理を簡略化する方法について、段階を追った手順を示したり、エラー処理を簡単で現代的にする方法の例を紹介したりするなど、さまざまな内容を盛り込んでいます。.

Kenny Kerr は、カナダを拠点とするコンピューター プログラマであり、Pluralsight の執筆者です。また、Microsoft MVP でもあります。彼のブログは kennykerr.ca (英語) で、Twitter は twitter.com/kennykerr (英語) でフォローできます。