ROS2講座09 SQLiteをC++から使う

# 環境

この記事は以下の環境で動いています。

| 項目 |	値 |
|:--|:--|
| CPU | Core i5-8250U |
| Ubuntu | 22.04 |
| ROS2 | Humble |
| SQLite | 3.37.2 |

# 概要
前回でc++でSQLiteを使う基礎的なコードを紹介しました。ここではSELECTの結果を取り出す、ステートメントでSQL命令を書くの2つのAPIの使い方を紹介します。

# SELECTの結果を取り出す
上記のサンプルではデータ操作をするコマンドを

## ソースコード

```c++:sqlite_lecture/src/sample2/main.cpp
#include <sqlite3.h>
#include <sqlite_lecture/relation_data.h>

int exec_callback(void *data, int argc, char **argv, char **azColName)
{
  RelationData *relation_data = (RelationData *)data;
  relation_data->increment();
  for (int i = 0; i < argc; i++)
  {
    relation_data->setAtBack(azColName[i], argv[i]);
  }
  return 0;
};

int main(int argc, char *argv[])
{
  printf("Start\n");

  sqlite3 *db = NULL;
  if (sqlite3_open("data.db", &db) != SQLITE_OK)
  {
    printf("%s[%s]\n", __func__, sqlite3_errmsg(db));
    return -1;
  }

  // create Table
  if (sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS CountList (name PRIMARY KEY, count);", NULL, NULL, NULL) != SQLITE_OK){
    printf("%s[%s]\n", __func__, sqlite3_errmsg(db));
    return -1;
  }

  // insert record
  if (sqlite3_exec(db, "INSERT OR IGNORE INTO CountList VALUES ('boot', 0);", NULL, NULL, NULL) != SQLITE_OK){
    printf("%s[%s]\n", __func__, sqlite3_errmsg(db));
    return -1;
  }

  // update record
  if (sqlite3_exec(db, "UPDATE CountList SET count = count + 1 WHERE name == 'boot'", NULL, NULL, NULL) != SQLITE_OK)
  {
    printf("%s[%s]\n", __func__, sqlite3_errmsg(db));
    return -1;
  }

  // get & view record
  RelationData relation_data;
  if (sqlite3_exec(db, "SELECT * FROM CountList", exec_callback, (void *)&relation_data, NULL))
  {
    printf("%s[%s]\n", __func__, sqlite3_errmsg(db));
    return -1;
  }
  relation_data.print();

  printf("Done\n");
  return 0;
}
```

* 前回のサンプルとの大きな違いは`sqlite3_exec(db, "SELECT * FROM CountList", exec_callback, (void *)&relation_data, NULL)`です。
    * 第3引数で結果を受け取るcallback関数の関数ポインタを入れます。
    * 第4引数でcallback関数で受け取る結果保存用の構造体のポインタを入れます。この値が`exec_callback`の第1引数で渡されます。
    * 結果のレコード(行)が複数ある時は、レコード数だけ`exec_callback`が呼ばれます。


<details><summary>RelationDataの実装</summary><div>

```c++:/home/ubuntu/ros2_ws/src/ros2_lecture/sqlite_lecture/include/sqlite_lecture/relation_data.h
#include <string>
#include <vector>
#include <map>

class RelationData
{
public:
  RelationData()
  {
  }

  void increment(void)
  {
    line_size_++;
    for (auto &c : data_)
    {
      c.second.push_back("");
    }
  }

  void setAtBack(std::string column_name, std::string value)
  {
    // new
    if (data_.count(column_name) == 0)
    {
      std::vector<std::string> column;
      column.resize(line_size_);
      data_[column_name] = column;
    }

    if (data_[column_name].empty())
    {
      printf("empty %s\n", column_name.c_str());
      return;
    }
    data_[column_name].back() = value;
  }

  std::string get(std::string column_name, size_t line) const
  {
    if (!checkSize())
    {
      return "";
    }
    if (data_.count(column_name) == 0)
    {
      printf("not found %s\n", column_name.c_str());
      return "";
    }
    if (line >= data_.at(column_name).size())
    {
      printf("no data %s %lu (should < %lu)\n", column_name.c_str(), line, data_.at(column_name).size());
      return "";
    }
    return data_.at(column_name)[line];
  }

  size_t size(void) const
  {
    if (!checkSize())
    {
      return 0;
    }
    return line_size_;
  }

  void print(void) const
  {
    if (!checkSize())
    {
      return;
    }
    printf("======table======\n");
    for (auto c : data_)
    {
      printf("[%s]: ", c.first.c_str());
      for (auto v : c.second)
      {
        printf("%s, ", v.c_str());
      }
      printf("\n");
    }
    printf("=================\n");
  }

private:
  bool checkSize(void) const
  {
    bool size_is_ok = true;
    for (auto c : data_)
    {
      if (line_size_ != c.second.size())
      {
        size_is_ok = false;
        printf("%s is %lu (expected %lu)\n", c.first.c_str(), c.second.size(), line_size_);
      }
    }
    return size_is_ok;
  }

  size_t line_size_{0};
  std::map<std::string, std::vector<std::string>> data_;
};
```
</div></details>

## ビルド&実行

```shell:ビルド
source /opt/ros/humble/setup.bash
cd ros2_ws
colcon build
```

```shell:実行
$ ros2 run sqlite_lecture sample2 
Start
======table======
[count]: 1, 
[name]: boot, 
=================
Done
$ ros2 run sqlite_lecture sample2 
Start
======table======
[count]: 2, 
[name]: boot, 
=================
Done
$ ros2 run sqlite_lecture sample2 
Start
======table======
[count]: 3, 
[name]: boot, 
=================
Done
```

実行する毎にbootの値がインクリメントされるのが分かります。

# ステートメントでSQLを使う

データの`INSERT`ではSQL命令中に入力するデータの値を入れる必要があります。文字列操作をしてこれらの値が入ったSQL文をユーザーが作成してもよいのですが、SQLiteのAPIにはステートメントという機能があります。これを使ってデータをinsertしてみます。

## ソースコード

```c++:sqlite_lecture/src/sample3/main.cpp
#include <sqlite3.h>
#include <sqlite_lecture/relation_data.h>

int exec_callback(void *data, int argc, char **argv, char **azColName)
{
  RelationData *relation_data = (RelationData *)data;
  relation_data->increment();
  for (int i = 0; i < argc; i++)
  {
    relation_data->setAtBack(azColName[i], argv[i]);
  }
  return 0;
};

int main(int argc, char *argv[])
{
  printf("Start\n");

  sqlite3 *db = NULL;
  if (sqlite3_open("data.db", &db) != SQLITE_OK)
  {
    printf("%s[%s]\n", __func__, sqlite3_errmsg(db));
    return -1;
  }

  // create Table
  if (sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS PositionList (name PRIMARY KEY, x, y);", NULL, NULL, NULL) != SQLITE_OK)
  {
    printf("%s[%s]\n", __func__, sqlite3_errmsg(db));
    return -1;
  }

  // insert record with statement
  char sql_raw[256] = "INSERT OR IGNORE INTO PositionList VALUES (?, ?, ?);";
  sqlite3_stmt *pStmt = NULL;
  if (sqlite3_prepare_v2(db, sql_raw, 256, &pStmt, NULL) != SQLITE_OK)
  {
    printf("%s[%s]\n", __func__, sqlite3_errmsg(db));
    return -1;
  }

  // 1st data
  sqlite3_bind_text(pStmt, 1, "pos1", -1, SQLITE_TRANSIENT);
  sqlite3_bind_double(pStmt, 2, 1.1);
  sqlite3_bind_double(pStmt, 3, 2.2);
  while (SQLITE_DONE != sqlite3_step(pStmt))
  {
  }
  sqlite3_reset(pStmt);

  // 2nd data
  sqlite3_bind_text(pStmt, 1, "pos2", -1, SQLITE_TRANSIENT);
  sqlite3_bind_double(pStmt, 2, 5.1);
  sqlite3_bind_double(pStmt, 3, 6.2);
  while (SQLITE_DONE != sqlite3_step(pStmt))
  {
  }
  sqlite3_finalize(pStmt);

  // get & view record
  RelationData relation_data;
  if (sqlite3_exec(db, "SELECT * FROM PositionList", exec_callback, (void *)&relation_data, NULL))
  {
    printf("%s[%s]\n", __func__, sqlite3_errmsg(db));
    return -1;
  }
  relation_data.print();

  printf("Done\n");
  return 0;
}
```

* `sqlite3_prepare_v2`でステートメントを準備します。この時に使うSQL文は一部が`?`になっています。
* `sqlite3_bind_double_text`で`?`に文字列を代入します。
    * 第2引数は何番目の?に代入するかというものです。1から数え始めることに注意です。
    * 第4引数は文字数ですが-1を入れるとnull終端までを見ます。
    * 第5引数の`SQLITE_TRANSIENT`は内部的に文字列をコピーするというオプションで、これを指定しておくのが無難です。
* `sqlite3_bind_double`では数字を`?`に代入します。
* `sqlite3_step`ではステートメントのSQLを実行します。
* ステートメントを使いまわすときは`sqlite3_reset`でbindの結果をリセットしてからもう一度bindします。
* ステートメントを使い終わったら`sqlite3_finalize`でクリアします。

## ビルド&実行
```shell:ビルド
source /opt/ros/humble/setup.bash
cd ros2_ws
colcon build
```

```shell:実行
ros2 run sqlite_lecture sample3 
Start
======table======
[name]: pos1, pos2, 
[x]: 1.1, 5.1, 
[y]: 2.2, 6.2, 
=================
Done

```

DBに保存したデータが読み出せています。

# 目次ページへのリンク
[ROS2講座の目次へのリンク](https://qiita.com/srs/items/5f0d765343ca78f524ed)