Elsaの技術日記(徒然なるままに)

主に自分で作ったアプリとかの報告・日記を記載

MENU

sqlite3でDBを操作してみる(C++編) ~~続き~~

先日C++sql文を利用使用してDB操作する方法をまとめましたが、
データ挿入まででデータ閲覧までは記事に載せきれませんでした。
elsammit-beginnerblg.hatenablog.com

今回はデータ閲覧方法についてまとめていきたいと思います!!
f:id:Elsammit:20210529125301j:plain



■sqlite3操作(データベースからデータを取得)

では引き続きデータ取得方法をまとめていきます。

まずは前回作成したデータベースをおさらいしておきます。
今回も前回と同様にこちらのデータベースを用いていきます。
f:id:Elsammit:20210529115441p:plain

データベースからデータを取得するコードはこちら。

#include <sqlite3.h>
#include <stdio.h>
#include <cstring>
#include <iostream>
#include <vector>

typedef struct{
    int id;
    std::string name;
    std::string category;
    int cost;
    int size;
    int weight;
} dbcolumn;

int callBackFunc(void* param, int col_cnt, char** row_txt, char** col_name){
    dbcolumn m_dbcolumn;
    m_dbcolumn.id = atoi(row_txt[0]);
    char* name = row_txt[1];
    char* category = row_txt[2];
    m_dbcolumn.name = std::string((char*)name);
    m_dbcolumn.category = std::string((char*)category);
    m_dbcolumn.cost = atoi(row_txt[3]);
    m_dbcolumn.size = atoi(row_txt[4]);
    m_dbcolumn.weight = atoi(row_txt[5]);
    
    ((std::vector<dbcolumn>*)param)->push_back(m_dbcolumn);
    
    return 0;
}

int main(void){
    sqlite3 *db = NULL;
    char* err = NULL;
    int count = 0;
    std::vector<dbcolumn> m_dbList;

    int ret = sqlite3_open("./hoge.db", &db);
    if(ret != SQLITE_OK){
        printf("FILE Open Error \n");
    }
    char sql2[30] = "select * from huga;";
    ret = sqlite3_exec(db, sql2, callBackFunc, (void*)(&m_dbList), &err);
    if(ret != SQLITE_OK){
        printf("DB Read Error \n");
        return -1;
    }

    sqlite3_close(db);
    printf("Finish \n");
    return 0;
}

例のごとくsql文の実行にはsqlite3_exec関数を用いています。
データ取得にあたり、データを格納する変数はdbcolumn構造体に格納しております。
データを格納する際のコールバック関数はこちらになっており、
各レコード毎に構造体変数に値を格納しています。

int callBackFunc(void* param, int col_cnt, char** row_txt, char** col_name){
    dbcolumn m_dbcolumn;
    m_dbcolumn.id = atoi(row_txt[0]);
    char* name = row_txt[1];
    char* category = row_txt[2];
    m_dbcolumn.name = std::string((char*)name);
    m_dbcolumn.category = std::string((char*)category);
    m_dbcolumn.cost = atoi(row_txt[3]);
    m_dbcolumn.size = atoi(row_txt[4]);
    m_dbcolumn.weight = atoi(row_txt[5]);
    
    ((std::vector<dbcolumn>*)param)->push_back(m_dbcolumn);
    
    return 0;
}

sqlite3_exec関数のコールバックですが、各行の読み込み毎にコールされます。
すなわち、select * fromでデータを取得した場合、
1行目読み込んだらコールバック関数コール、
2行目読み込んだらコールバック関数コール、
・・・
といったような制御になっております。

コールバック関数に与えている引数ですがそれぞれ、
param:コールバックコールに渡す引数(nullでも可)
col_cnt:行数.
row_txt:行毎のデータ.
col_name:列名.
となります。

こちらを実行するとデータの読み取りも行えるかと思います。

■prepared_statementを使用してデータを取得する

上記のようにsqlite3_exec関数を用いればデータの取得は可能です。
が、、、
繰り返しsql文を実行する場合にはprepared_statementを用いた方がパフォーマンスが向上するようです。
どうやら、sqlite3_execは毎回内部的にprepared_statementを生成・実行しているようで繰り返し処理の場合には無駄な処理が入ってしまいパフォーマンスが落ちてしまうよう。

ということで、prepared_statementを使用した場合についてもまとめておきます。
コードはこちら。

#include <sqlite3.h>
#include <stdio.h>
#include <cstring>
#include <iostream>
#include <vector>

typedef struct{
    int id;
    std::string name;
    std::string category;
    int cost;
    int size;
    int weight;
} dbcolumn;

int main(void){
    sqlite3 *db = NULL;
    char* err = NULL;
    int count = 0;
    sqlite3_stmt *stmt = NULL;
    std::vector<dbcolumn> m_dbList;

    int ret = sqlite3_open("./hoge.db", &db);
    if(ret != SQLITE_OK){
        printf("FILE Open Error \n");
    }
    char sql2[30] = "select * from huga;";
    sqlite3_prepare_v2(db, sql2, strlen(sql2), &stmt, NULL);

    sqlite3_reset(stmt);

    int r = 0;
    dbcolumn m_dbcolumn;
    while (SQLITE_ROW == (r=sqlite3_step(stmt))){

        // 数値は列数に対応し、各行に対するどの列を読み出すか指定が可能
        m_dbcolumn.id = sqlite3_column_int(stmt, 0);                    // sqlite3_column_intを使用すればint型で定義したデータが読み込める.
        const unsigned char* name = sqlite3_column_text(stmt, 1);       // sqlite3_column_textを使用すればtext型で定義したデータが読み込める
        const unsigned char* category = sqlite3_column_text(stmt, 2);
        m_dbcolumn.name = std::string((char*)name);
        m_dbcolumn.category = std::string((char*)category);
        m_dbcolumn.cost = sqlite3_column_int(stmt, 3);
        m_dbcolumn.size = sqlite3_column_int(stmt, 4);
        m_dbcolumn.weight = sqlite3_column_int(stmt, 5);

        m_dbList.push_back(m_dbcolumn);
    }
    if (SQLITE_DONE!=r){
        printf("Read Error \n");
        return -1;
    }
 sqlite3_finalize(stmt );
    sqlite3_close(db);
    printf("Finish \n");
    return 0;
}

こちらのsqlite3_prepare_v2関数を用いることでprepared_statementを生成しクエリの実行が出来ます。

sqlite3_prepare_v2(db, sql2, strlen(sql2), &stmt, NULL);

sqlite3_prepare関数もあるのですが、レガシーな関数であり現在は使用することをお勧めされていないようです。
http://www.sqlite.org/c3ref/prepare.html

prepared_statementは1回実行するたびに初期化する必要があります。
sqlite3_reset()関数を用いることでprepared_statementを実行可能な状態にリセットさせることが出来ます。

取得したデータを閲覧している関数がこちらです。

    while (SQLITE_ROW == (r=sqlite3_step(stmt))){
        // 数値は列数に対応し、各行に対するどの列を読み出すか指定が可能
        m_dbcolumn.id = sqlite3_column_int(stmt, 0);                    // sqlite3_column_intを使用すればint型で定義したデータが読み込める.
        const unsigned char* name = sqlite3_column_text(stmt, 1);       // sqlite3_column_textを使用すればtext型で定義したデータが読み込める
        const unsigned char* category = sqlite3_column_text(stmt, 2);
        m_dbcolumn.name = std::string((char*)name);
        m_dbcolumn.category = std::string((char*)category);
        m_dbcolumn.cost = sqlite3_column_int(stmt, 3);
        m_dbcolumn.size = sqlite3_column_int(stmt, 4);
        m_dbcolumn.weight = sqlite3_column_int(stmt, 5);

        m_dbList.push_back(m_dbcolumn);
    }

sqlite_step関数でSQL文の結果から1行ずつ読み取ります。
読み取ったデータですが、
int型のデータの場合には、sqlite3_column_int関数
text型のデータの場合には、sqlite3_column_text関数
を用いるようです。

またsqlite3_column_int関数、sqlite3_column_text関数の第2引数には取得したい列番号を指定すればよいです。

最後にsqlite3_finalize関数を用いて生成したprepared_statementを削除して完了です。

こちらのコードを実行しても同様にデータ取得できるかと思います。
prepared_statementを利用した場合、sqlite3_exec関数と比較してどの程度パフォーマンス向上するのかは分かりませんでした。。。
もう少し調べてみたいと思います!!

■最後に

今回はデータベースからのデータ読み出しですが、
sqlite3_exec関数を用いたケースとprepared_statementを利用したケースの2つを載せました。
一般的にsqlite3_exec関数はCREATE_TABLEなど初回のみ実行のケース。
select * fromなど複数回実行するケースはprepared_statementを用いるようです。

prepared_statementは初期化やリセット・解放する変数が1つ増えるため注意が必要なのが困りものですね。。。