刹那(せつな)の瞬き

Willkömmen! Ich heiße Setsuna. Haben Sie etwas Zeit für mich?

UbuntuでnanodbcからSQLServerに接続する #3 - 追試編

導入編では nanodbc をビルドして Ubuntu 環境にインストールしました。
ここでは、日本語を含むテーブル・カラム・クエリ等の扱いと、その他、気になる点について追試してみます。

対象は引き続き MS 版 ODBC ドライバです。

1. 追試用プロジェクトの準備

導入編で nanodbc をインストールした状態が前提です。
その続きとして、別途 nanodbc の共有ライブラリを利用する環境を整えます。

(1) 追試用ディレクト

既に work ディレクトリが存在するとして、新たに my_test ディレクトリを作成します。

$ cd ~/work
$ mkdir my_test
$ cd my_test
(2) ソースコード

my_test ディレクトリに、CMakeLists.txtmain.cppを作成します。

・CMakeLists.txt

cmake_minimum_required(VERSION 3.0)
project(my_test CXX)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(nanodbc REQUIRED)

add_executable(a.out main.cpp)
target_compile_options(a.out PRIVATE -g -Wall)
target_link_libraries(a.out PRIVATE nanodbc)

・main.cpp

#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <nanodbc/nanodbc.h>

int main()
{
    std::locale::global(std::locale(""));   // for ODBC driver
    try
    {
        nanodbc::connection conn("Driver={ODBC Driver 17 for SQL Server};Server=localhost;UID=sa;PWD=abcd1234$;Database=my_test_db");
        std::cout << "確認 #1"  << std::endl;
        {
            // varchar は文字化けする
            nanodbc::result rs1 = nanodbc::execute(conn, NANODBC_TEXT("SELECT '社員コード'"));
            rs1.next();
            std::cout << "varchar として処理 : [" << rs1.get<std::string>(0) << "]" << std::endl;
            // nvarchar は正常
            nanodbc::result rs2 = nanodbc::execute(conn, NANODBC_TEXT("SELECT N'社員コード'"));
            rs2.next();
            std::cout << "nvarchar として処理 : [" << rs2.get<std::string>(0) << "]" << std::endl;
            // NULL値
            nanodbc::result rs3 = nanodbc::execute(conn, NANODBC_TEXT("SELECT NULL"));
            rs3.next();
            std::cout << "NULL値を取得 : " << rs3.get<std::string>(0, "< null >を取得") << std::endl;
        }
        std::cout << "確認 #2" << std::endl;
        {
            // テーブルの準備
            nanodbc::execute(conn, NANODBC_TEXT("DROP TABLE IF EXISTS 社員"));
            nanodbc::execute(conn, NANODBC_TEXT(
                "CREATE TABLE 社員 (コード int, 氏名 nvarchar(40), 入社日 date, 備考 nvarchar(max))"
            ));
            // ローの準備
            std::vector<std::vector<std::string>> rec = {
                { "210", "成宮 真紀", "1991-04-01" },
                { "110", "加藤 泰江", "1990-04-01" },
                { "105", "森上 偉久馬", "1990-04-01" },
                { "304", "山本 雅治", "1989-04-01" },
                { "307", "小川 さよ子", "1987-04-01" },
                { "305", "青木 俊之", "1988-04-01" },
                { "207", "松沢 誠一", "1994-04-01" },
                { "107", "葛城 孝史", "1991-09-01" },
                { "204", "川村 匡", "1990-04-01" },
            };
            // プレースホルダを経由して、ローの値を INSERT
            nanodbc::statement stmt(conn);
            nanodbc::prepare(stmt, NANODBC_TEXT("INSERT INTO 社員 (コード,氏名,入社日) VALUES (?,?,?)"));
            for (const std::vector<std::string>& param : rec) {
                stmt.bind(0, param.at(0).c_str());
                stmt.bind(1, param.at(1).c_str());
                stmt.bind(2, param.at(2).c_str());
                nanodbc::execute(stmt);
            }
            // テーブル内容の表示
            nanodbc::string qry(NANODBC_TEXT("SELECT コード,氏名,入社日 FROM 社員 ORDER BY コード"));
            nanodbc::result rs = nanodbc::execute(conn, qry);
            // この時点で affected_rows() == -1
            std::cout << "結果 " << rs.affected_rows() << " 行, rowset size: " << rs.rowset_size() << std::endl;
            // カラム名表示
            std::cout << rs.column_name(0) << " | " << rs.column_name(1) << " | " << rs.column_name(2) << std::endl;
            // 取得ロー表示
            for (const auto& row : rs) {
                auto val = row.get<int>(0);
                auto name = row.get<nanodbc::string>(1);
                auto d = row.get<nanodbc::date>(2);
                std::cout << val << " | " << name << " | " << d.year << "." <<  d.month << "." << d.day << std::endl;
            }
            // この時点で affected_rows() == 取得ロー数
            std::cout << "結果 " << rs.affected_rows() << " 行, rowset size: " << rs.rowset_size() << std::endl;
        }
        std::cout << "確認 #3"  << std::endl;
        {
            // プレースホルダを経由して、ローの値を UPDATE
            nanodbc::statement stmt(conn);
            nanodbc::prepare(stmt, NANODBC_TEXT("UPDATE 社員 SET 備考=? WHERE コード=?"));
            // 生文字列リテラル
            std::string memo1 = R"(【自己紹介】
私の故郷は、富士五湖のひとつである本栖湖のほとりにあり、実家はキャンプ場を経営しています。

年中無休なので、子供の頃からよく手伝わされました。
特に夏のシーズンは忙しく、夏休みに家族で旅行に行った記憶はありません。
)";
            stmt.bind(0, memo1.c_str());
            stmt.bind(1, "210");
            nanodbc::execute(stmt);
            // 絵文字を含む文字列を連結 ※連結コストは無視
            std::list<std::string> sl;
            sl.push_back("【自己紹介】\n");
            sl.push_back("私は、スポーツが好きで、特にサッカー" u8"\U000026BD\U0001F945" "は見るのもするのも夢中になります。\n");
            sl.push_back("\n");
            sl.push_back("並行して、家が以前、エレクトーン" u8"\U0001F3B9" "の教室を開いていたので、楽器もよく演奏したりしていました。\n");
            std::string memo2;
            for (const auto& line : sl) {
                memo2 += line;
            }
            const int val = 110;
            stmt.bind(0, memo2.c_str());
            stmt.bind(1, &val);
            nanodbc::execute(stmt);
            // テーブル内容の表示
            nanodbc::string qry(NANODBC_TEXT("SELECT コード,氏名,備考 FROM 社員 WHERE 備考 IS NOT NULL"));
            nanodbc::result rs = nanodbc::execute(conn, qry);
            for (const auto& row : rs) {
                std::cout << row.get<int>("コード") << " : " << row.get<nanodbc::string>("氏名") << std::endl;
                auto prof = row.get<nanodbc::string>("備考");
                std::cout << ((prof.length() > 0) ? prof : "なし") << std::endl;
            }
        }
        std::cout << "確認 #4"  << std::endl;
        {
            // 氏名を限定して検索
            nanodbc::statement stmt(conn);
            nanodbc::string sql_text(NANODBC_TEXT("SELECT コード,氏名,入社日 FROM 社員 WHERE 氏名 LIKE ? OR 氏名 LIKE ?"));
            nanodbc::prepare(stmt, sql_text);
            stmt.bind(0, "%泰%");
            stmt.bind(1, "%川%");
            nanodbc::result rs = nanodbc::execute(stmt);
            // 結果セットを保存
            struct temp {
                int id;
                nanodbc::string name;
                nanodbc::string joined;
            };
            std::vector<temp> target;
            for (const auto& row : rs) {
                target.push_back({
                    row.get<int>("コード"),
                    row.get<nanodbc::string>("氏名"),
                    row.get<nanodbc::string>("入社日"),
                });
            }
            // 保存した順に表示
            std::cout << "1回目" << std::endl;
            for (const auto& d : target) {
                std::cout << d.id << " | " << d.name << " | " << d.joined << std::endl;
            }
            // 逆順で表示
            std::cout << "2回目" << std::endl;
            for (int i = target.size(); i > 0; i--) {
                const auto& d = target.at(i - 1);
                std::cout << d.id << " | " << d.name << " | " << d.joined << std::endl;
            }
        }
    }
    catch (const std::exception& e)
    {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

このソースコードは、私が Qt5 で書いたテストコードを部分的に流用してます。
その為、一部に不自然な記述もありますが、今回の追試には影響ないはずです。

(3) ビルド用ディレクトリの準備

my_test ディレクトリにビルド用のディレクトリを作成します。

$ pwd
/home/dareka/work/my_test
$ mkdir build
$ cd build

以降の操作は、このディレクトリで作業します。

2. ビルドと実行

(1) Makefile 生成と make

まずはプロジェクトをコンパイルして実行ファイルを生成します。

$ cmake ..
$ make
(2) 実行

ビルドが成功すると、a.outが生成されるので、これを実行します。
テスト編と同様に、実行時の接続先データベースは 'my_test_db' です。

$ ./a.out

3. 実行結果の確認

確認したい事を 4 つのブロックに分けて処理しました。
結果は、すべて想定内・期待通り、でした。 

(1) 単一の値を返すクエリの確認

std::cout << "確認 #1" << std::endl;以降のブロックです。

SELECT 文に値を直接指定して nanodbc::result で受けてます。
N' ' で括られた日本語を含む文字列リテラルは暗黙に nvarchar として扱われ、期待通りに動作します。一方、' ' で括られた方は暗黙に varchar として扱われ、文字化けしているように見えます。

(2) 日本語でカラムやテーブルを定義してローを挿入する

std::cout << "確認 #2" << std::endl;以降のブロックです。

まず、日本語を含むテーブルを作成します。
そこに、日本語を含む値をプレースホルダにバインドして INSERT してます。
全件 SELECT して表示する際、affected_row() の変化にも注目してみました。

(3) nvarchar(max)型で日本語や絵文字を含むテキストを扱う

std::cout << "確認 #3" << std::endl;以降のブロックです。

生文字列リテラルとstd::stringを連結したテキストデータが期待通りに設定・再取得できる事を確認してます。
敢えて異なる方法でバインドして、それぞれ正常に処理されてます。

Ubuntu では絵文字を含めて期待通りに処理されました。
WindowsmacOS 等が混在した状態では試してません。

(4) LIKE 演算子へのパラメータと結果セットの再利用

std::cout << "確認 #4" << std::endl;以降のブロックです。

LIKE 演算子に対する日本語を含むパラメータバインドのテストが無かったので、試してみました。
結果セットを構造体の動的配列に退避して、値の再利用も試してます。

4. まとめ

とりあえず気になった点については、自分なりに納得できる結果でした。
nanodbc で用意されているテストは網羅する範囲が広く、標準のテストに追加する仕組みも用意されているので、とても助かります。

今回は日本語が絡む為、別途プロジェクトを用意しての追試です。

実際のところ、テーブル名やカラム名に日本語を採用した現場に遭遇した事はありません。それでも、クエリと結果セットに日本語が含まれる可能性が高い為、今回のような日本語を多用するケースを敢えて検証しました。

UTF-8 にしろ UTF-16 にしろ、現在の文字コードの混沌に立ち向かえる程、私は強くないですが、せめて自分の足元くらいは確認しておきたいと思ってます。

記事のリスト

ついつい熱が入った紹介になりましたが、本当に便利だと思います。

私の場合、C++ODBC を扱うコードを書くなら Qt5 を最優先にしてましたが、これなら nanodbc も十分選択肢に入ります。