C言語DB入門|データベースから書籍を読み込む

C言語DB入門|データベースから書籍を読み込む

前の記事では、第6章の作業フォルダにbooks.dbを作成し、booksテーブルを準備してから入力画面を表示しました。

今回は、そのデータベースから書籍情報を読み込みます。

使用するのは、PartⅠでも学んだSELECTです。ただし、読み込んだ内容をその場で表示するだけでなく、あとからGUIで利用できるように、C言語の構造体へ保存するところまで進めます。

この記事では、次の内容を学びます。

  • 読み込み確認用の書籍を用意する。
  • SELECTで書籍をIDの昇順に取得する。
  • sqlite3_stepで結果を1行ずつ読み込む。
  • 取得した情報をBook構造体へ保存する。
  • 読み込みの成功、0件、失敗を区別する。
  • 使用したステートメントとメモリを解放する。

今回はGUIに読み込み件数を表示し、書籍の内容はターミナルで確認します。GUIの一覧へ表示する処理は、次の記事で追加します。

読み込み処理と表示処理を分ける

今回作成するload_booksは、データベースから書籍を読み込む関数です。

この関数の中では、ラベルの更新やターミナルへの表示は行いません。

担当する処理は、次の3つです。

  1. SELECTを実行する。
  2. 取得した書籍情報をメモリへ保存する。
  3. 読み込んだデータと処理結果を呼び出し元へ渡す。

呼び出し元は、そのデータを使ってターミナルへ表示したり、GUIの表示を更新したりします。

処理担当
SQLの準備と実行load_books
書籍情報の取得とコピーload_books
ターミナルへの表示print_loaded_books
読み込み件数の画面表示create_main_window
読み込んだデータの解放アプリの終了処理

表示方法が変わっても、load_booksを利用できる構成にしていきましょう。

図1:書籍情報を読み込んで表示側へ渡す流れ

この図から分かること

データベースから取得した書籍情報を、まずメモリへ保存します。

そのデータを呼び出し元へ渡すことで、ターミナルでの確認にも、今後のGUI一覧表示にも利用できます。

第6章のbooks.dbに確認用の書籍を用意する

前の記事で新しく作成したbooks.dbには、まだ書籍が登録されていません。

0件の読み込みも正常な動作ですが、複数行を取得する流れを確認するため、次の2件を用意しましょう。

タイトル著者名
C言語入門山田太郎
SQLite入門鈴木花子

今回は、読み込み用のCプログラムとは別に、SQLファイルで準備します。

アプリの起動処理に書籍の追加を組み込まないため、起動するたびに確認用データが増えることはありません。

同じ書籍の重複追加を避けて確認用データを用意するSQL

ファイル名: prepare_reading_books.sql

BEGIN TRANSACTION;

INSERT INTO books (title, author)
SELECT 'C言語入門', '山田太郎'
WHERE NOT EXISTS (
    SELECT 1
    FROM books
    WHERE title = 'C言語入門'
      AND author = '山田太郎'
);

INSERT INTO books (title, author)
SELECT 'SQLite入門', '鈴木花子'
WHERE NOT EXISTS (
    SELECT 1
    FROM books
    WHERE title = 'SQLite入門'
      AND author = '鈴木花子'
);

COMMIT;

SELECT id, title, author
FROM books
ORDER BY id;

このSQLは、同じタイトルと著者名の組み合わせが存在しない場合だけ追加します。

繰り返し実行しても、その組み合わせの書籍を重ねて追加しないための、確認用データに限った工夫です。アプリ全体に重複登録禁止の制約を追加するものではありません。

SQL中の単一引用符は、文字列を表すために必要な構文です。

最後に、作られた行を登録する

INSERT INTO books (title, author)

SELECT が1行を作った場合、その1つ目の値を title、2つ目の値を author に入れます。SELECT が行を作らなかった場合は、何も登録しません。

books にあるデータこのSQLの結果
「C言語入門/山田太郎」がない1件登録する
「C言語入門/山田太郎」がある登録しない
「C言語入門/別の著者」だけがある著者名が違うため、1件登録する

つまり、タイトルと著者名の組み合わせを確認してから追加するSQLです。ただし、このSQLは既にある重複行を削除するものではありません。

補足

このSQLは、books テーブルに「C言語入門/山田太郎」がまだ存在しないときだけ、1件登録する処理です。

INSERT INTO books (title, author)
SELECT 'C言語入門', '山田太郎'
WHERE NOT EXISTS (
    SELECT 1
    FROM books
    WHERE title = 'C言語入門'
      AND author = '山田太郎'
);
まず、同じ書籍があるか調べる

内側の部分が存在確認です。

SELECT 1
FROM books
WHERE title = 'C言語入門'
  AND author = '山田太郎'

books の中から、タイトルと著者名の両方が一致する行を探します。SELECT 1 の 1 は存在確認用の値で、書籍のIDや登録件数ではありません。ここでは「該当する行が見つかるか」だけが重要です。

見つからなければ、挿入する行を作る
SELECT 'C言語入門', '山田太郎'
WHERE NOT EXISTS (...)

NOT EXISTS は「内側の検索で行が見つからなければ」という意味です。

  • 見つからない場合:SELECT が 'C言語入門', '山田太郎' という1行を作ります。
  • 見つかる場合:SELECT は行を作りません。

ここでの SELECT は、既存の書籍を取り出すためではなく、挿入する値を1行として作るために使われています。

なぜ、以下のようにしないのか?

INSERT INTO books (title, author)
VALUES ('C言語入門', '山田太郎')
WHERE NOT EXISTS (...);  -- ここにWHEREは置けない

その書き方にしない理由は、SQLiteの文法では VALUES (...) の直後に WHERE を置けないからです。

SQLファイルを実行する

前の記事のアプリを終了し、prepare_reading_books.sqlをC:\c-db\chap06へUTF-8で保存します。

MSYS2 UCRT64で、第6章の作業フォルダへ移動してください。

cd /c/c-db/chap06

前の記事で作成したbooks.dbに対して実行します。

sqlite3 books.db < prepare_reading_books.sql

このSQLは、前の記事でbooksテーブルを作成済みであることが前提です。

実行結果の例

新しく作成した空のbooksテーブルに初めて追加した場合は、次のように表示されます。

1|C言語入門|山田太郎
2|SQLite入門|鈴木花子

既存のデータや過去の登録・削除がある場合、IDや表示件数は異なります。

1件の書籍をBook構造体で表す

読み込んだ書籍情報は、Book構造体へ保存します。

typedef struct {
    sqlite3_int64 id;
    char *title;
    char *author;
} Book;
メンバー保存する内容
id書籍ID
titleコピーしたタイトル文字列へのポインタ
authorコピーした著者名文字列へのポインタ

IDには、SQLiteの64ビット整数を扱うsqlite3_int64を使用します。

titleとauthorには、自分たちでコピーした文字列を保持します。SQLiteから受け取ったポインタを、そのまま長期間保存する構成にはしません。

複数の書籍をGPtrArrayへ保存する

書籍の件数は、データベースの内容によって変わります。

そこで今回は、GLibが提供するGPtrArrayを使用します。GPtrArrayは、ポインタを格納できる、要素数を増やせる配列です。

各要素には、1件分のBookへのポインタを保存します。

要素保存するもの
要素01件目のBookへのポインタ
要素12件目のBookへのポインタ
要素23件目のBookへのポインタ

件数はlenで確認し、要素はg_ptr_array_indexで取り出します。

GTK 4のプログラムではGLibも利用できるため、今回のコンパイル指定はgtk4とsqlite3のままで進められます。

書籍を読み込むプログラム全体

前の記事の起動処理に、load_booksを追加します。

接続とテーブル準備のあとに書籍を読み込み、成功したら内容をターミナルへ表示し、GUIに件数を表示します。

SQLiteから書籍を読み込み、内容と件数を確認するプログラム

ファイル名: book_database_load.c

#include <gtk/gtk.h>
#include <sqlite3.h>
#include <stdlib.h>

#define DATABASE_PATH "books.db"

typedef struct {
    sqlite3_int64 id;
    char *title;
    char *author;
} Book;

typedef struct {
    GtkWidget *title_entry;
    GtkWidget *author_entry;
    GtkWidget *result_label;
} AppWidgets;

typedef struct {
    AppWidgets widgets;
    sqlite3 *db;
    GPtrArray *books;
    int exit_status;
} AppData;

static int open_database(const char *path, sqlite3 **db)
{
    return sqlite3_open(path, db);
}

static int initialize_database(sqlite3 *db)
{
    const char *sql =
        "CREATE TABLE IF NOT EXISTS books ("
        "id INTEGER PRIMARY KEY AUTOINCREMENT,"
        "title TEXT NOT NULL,"
        "author TEXT NOT NULL"
        ");";

    return sqlite3_exec(db, sql, NULL, NULL, NULL);
}

static int close_database(sqlite3 **db)
{
    int rc = sqlite3_close(*db);

    if (rc == SQLITE_OK) {
        *db = NULL;
    }

    return rc;
}

static void free_book(gpointer data)
{
    Book *book = data;

    g_free(book->title);
    g_free(book->author);
    g_free(book);
}

static int load_books(sqlite3 *db, GPtrArray **out_books)
{
    const char *sql =
        "SELECT id, title, author "
        "FROM books "
        "ORDER BY id;";

    sqlite3_stmt *stmt = NULL;
    *out_books = NULL;

    int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);

    if (rc != SQLITE_OK) {
        return rc;
    }

    GPtrArray *books =
        g_ptr_array_new_with_free_func(free_book);

    while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
        sqlite3_int64 id = sqlite3_column_int64(stmt, 0);

        const unsigned char *title =
            sqlite3_column_text(stmt, 1);

        if (title == NULL && sqlite3_errcode(db) == SQLITE_NOMEM) {
            rc = SQLITE_NOMEM;
            break;
        }

        const unsigned char *author =
            sqlite3_column_text(stmt, 2);

        if (author == NULL && sqlite3_errcode(db) == SQLITE_NOMEM) {
            rc = SQLITE_NOMEM;
            break;
        }

        Book *book = g_new0(Book, 1);

        book->id = id;

        book->title = g_strdup(
            title != NULL ? (const char *)title : ""
        );

        book->author = g_strdup(
            author != NULL ? (const char *)author : ""
        );

        g_ptr_array_add(books, book);
    }

    int finalize_rc = sqlite3_finalize(stmt);

    if (rc == SQLITE_DONE) {
        rc = finalize_rc;
    }

    if (rc != SQLITE_OK) {
        g_ptr_array_unref(books);
        return rc;
    }

    *out_books = books;

    return SQLITE_OK;
}

static void print_loaded_books(GPtrArray *books)
{
    if (books->len == 0) {
        g_print("登録されている書籍はありません。\n");
        return;
    }

    g_print("ID | タイトル | 著者名\n");

    for (guint i = 0; i < books->len; i++) {
        const Book *book = g_ptr_array_index(books, i);

        g_print(
            "%lld | %s | %s\n",
            (long long)book->id,
            book->title,
            book->author
        );
    }

    g_print("読み込み件数:%u件\n", books->len);
}

static void on_clear_clicked(GtkButton *button, gpointer user_data)
{
    (void)button;

    AppWidgets *widgets = user_data;

    gtk_editable_set_text(
        GTK_EDITABLE(widgets->title_entry),
        ""
    );

    gtk_editable_set_text(
        GTK_EDITABLE(widgets->author_entry),
        ""
    );

    gtk_label_set_text(
        GTK_LABEL(widgets->result_label),
        "入力欄をクリアしました。"
    );

    gtk_widget_grab_focus(widgets->title_entry);
}

static void on_error_close_clicked(
    GtkButton *button,
    gpointer user_data
)
{
    (void)button;

    gtk_window_close(GTK_WINDOW(user_data));
}

static void show_startup_error(
    GtkApplication *app,
    const char *message,
    const char *detail
)
{
    GtkWidget *window = gtk_application_window_new(app);

    gtk_window_set_title(GTK_WINDOW(window), "起動エラー");
    gtk_window_set_default_size(GTK_WINDOW(window), 600, 240);

    GtkWidget *box =
        gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);

    gtk_widget_set_margin_top(box, 24);
    gtk_widget_set_margin_bottom(box, 24);
    gtk_widget_set_margin_start(box, 24);
    gtk_widget_set_margin_end(box, 24);

    gtk_window_set_child(GTK_WINDOW(window), box);

    char *text = g_strdup_printf(
        "%s\n\n%s\n\n接続先:%s",
        message,
        detail,
        DATABASE_PATH
    );

    GtkWidget *label = gtk_label_new(text);
    g_free(text);

    gtk_label_set_wrap(GTK_LABEL(label), TRUE);
    gtk_label_set_xalign(GTK_LABEL(label), 0.0f);

    GtkWidget *close_button =
        gtk_button_new_with_label("閉じる");

    gtk_widget_set_halign(close_button, GTK_ALIGN_END);

    gtk_box_append(GTK_BOX(box), label);
    gtk_box_append(GTK_BOX(box), close_button);

    g_signal_connect(
        close_button,
        "clicked",
        G_CALLBACK(on_error_close_clicked),
        window
    );

    gtk_window_present(GTK_WINDOW(window));
}

static void create_main_window(
    GtkApplication *app,
    AppWidgets *widgets,
    guint book_count
)
{
    GtkWidget *window = gtk_application_window_new(app);

    gtk_window_set_title(GTK_WINDOW(window), "書籍管理アプリ");
    gtk_window_set_default_size(GTK_WINDOW(window), 640, 300);

    GtkWidget *main_box =
        gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);

    gtk_widget_set_margin_top(main_box, 24);
    gtk_widget_set_margin_bottom(main_box, 24);
    gtk_widget_set_margin_start(main_box, 24);
    gtk_widget_set_margin_end(main_box, 24);

    gtk_window_set_child(GTK_WINDOW(window), main_box);

    GtkWidget *input_grid = gtk_grid_new();

    gtk_grid_set_row_spacing(GTK_GRID(input_grid), 12);
    gtk_grid_set_column_spacing(GTK_GRID(input_grid), 12);

    GtkWidget *title_label = gtk_label_new("タイトル");
    GtkWidget *author_label = gtk_label_new("著者名");

    gtk_label_set_xalign(GTK_LABEL(title_label), 0.0f);
    gtk_label_set_xalign(GTK_LABEL(author_label), 0.0f);

    widgets->title_entry = gtk_entry_new();
    widgets->author_entry = gtk_entry_new();

    gtk_entry_set_placeholder_text(
        GTK_ENTRY(widgets->title_entry),
        "例:C言語入門"
    );

    gtk_entry_set_placeholder_text(
        GTK_ENTRY(widgets->author_entry),
        "例:山田太郎"
    );

    gtk_widget_set_hexpand(widgets->title_entry, TRUE);
    gtk_widget_set_hexpand(widgets->author_entry, TRUE);

    gtk_grid_attach(
        GTK_GRID(input_grid), title_label, 0, 0, 1, 1
    );

    gtk_grid_attach(
        GTK_GRID(input_grid), widgets->title_entry, 1, 0, 1, 1
    );

    gtk_grid_attach(
        GTK_GRID(input_grid), author_label, 0, 1, 1, 1
    );

    gtk_grid_attach(
        GTK_GRID(input_grid), widgets->author_entry, 1, 1, 1, 1
    );

    GtkWidget *button_box =
        gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);

    gtk_widget_set_halign(button_box, GTK_ALIGN_START);

    GtkWidget *add_button = gtk_button_new_with_label("登録");
    GtkWidget *update_button = gtk_button_new_with_label("変更");
    GtkWidget *delete_button = gtk_button_new_with_label("削除");
    GtkWidget *clear_button = gtk_button_new_with_label("クリア");

    gtk_widget_set_sensitive(add_button, FALSE);
    gtk_widget_set_sensitive(update_button, FALSE);
    gtk_widget_set_sensitive(delete_button, FALSE);

    gtk_box_append(GTK_BOX(button_box), add_button);
    gtk_box_append(GTK_BOX(button_box), update_button);
    gtk_box_append(GTK_BOX(button_box), delete_button);
    gtk_box_append(GTK_BOX(button_box), clear_button);

    char *message;

    if (book_count == 0) {
        message = g_strdup(
            "読み込みは正常に完了しました。\n"
            "登録されている書籍はありません。"
        );
    } else {
        message = g_strdup_printf(
            "%u件の書籍を読み込みました。\n"
            "内容はターミナルで確認できます。",
            book_count
        );
    }

    widgets->result_label = gtk_label_new(message);
    g_free(message);

    gtk_label_set_xalign(GTK_LABEL(widgets->result_label), 0.0f);
    gtk_label_set_wrap(GTK_LABEL(widgets->result_label), TRUE);

    gtk_box_append(GTK_BOX(main_box), input_grid);
    gtk_box_append(GTK_BOX(main_box), button_box);
    gtk_box_append(GTK_BOX(main_box), widgets->result_label);

    g_signal_connect(
        clear_button,
        "clicked",
        G_CALLBACK(on_clear_clicked),
        widgets
    );

    gtk_window_present(GTK_WINDOW(window));
    gtk_widget_grab_focus(widgets->title_entry);
}

static void on_activate(GtkApplication *app, gpointer user_data)
{
    AppData *data = user_data;

    GtkWindow *existing_window =
        gtk_application_get_active_window(app);

    if (existing_window != NULL) {
        gtk_window_present(existing_window);
        return;
    }

    int rc = open_database(DATABASE_PATH, &data->db);

    if (rc != SQLITE_OK) {
        data->exit_status = EXIT_FAILURE;

        const char *detail =
            data->db != NULL
                ? sqlite3_errmsg(data->db)
                : sqlite3_errstr(rc);

        show_startup_error(
            app,
            "データベースを開けませんでした。",
            detail
        );

        return;
    }

    rc = initialize_database(data->db);

    if (rc != SQLITE_OK) {
        data->exit_status = EXIT_FAILURE;

        show_startup_error(
            app,
            "booksテーブルを準備できませんでした。",
            sqlite3_errmsg(data->db)
        );

        return;
    }

    rc = load_books(data->db, &data->books);

    if (rc != SQLITE_OK) {
        data->exit_status = EXIT_FAILURE;

        show_startup_error(
            app,
            "書籍を読み込めませんでした。",
            sqlite3_errstr(rc)
        );

        return;
    }

    print_loaded_books(data->books);

    create_main_window(
        app,
        &data->widgets,
        data->books->len
    );
}

int main(int argc, char *argv[])
{
    AppData data = {0};

    GtkApplication *app = gtk_application_new(
        "com.example.cdb.BookDatabaseLoad",
        G_APPLICATION_DEFAULT_FLAGS
    );

    g_signal_connect(
        app,
        "activate",
        G_CALLBACK(on_activate),
        &data
    );

    int status = g_application_run(
        G_APPLICATION(app),
        argc,
        argv
    );

    g_object_unref(app);

    if (data.books != NULL) {
        g_ptr_array_unref(data.books);
        data.books = NULL;
    }

    int close_rc = close_database(&data.db);

    if (close_rc != SQLITE_OK) {
        g_printerr(
            "データベースを閉じられませんでした:%s\n",
            sqlite3_errstr(close_rc)
        );

        data.exit_status = EXIT_FAILURE;
    }

    if (data.exit_status != EXIT_SUCCESS) {
        return data.exit_status;
    }

    return status;
}

#include <gtk/gtk.h>のincludePathエラーが出る場合

以下の記事を参考にしてください。

C言語GUI入門|#include <gtk gtk.h="">のincludePathエラー</gtk>

C言語GUI入門|#include <gtk/gtk.h>のincludePathエラー このエラーは、VS Code の C/C++拡張機能(IntelliSense)が gtk/gtk.h の場所を見つけられていないときに…

コンパイルして実行する

book_database_load.cをC:\c-db\chap06へUTF-8で保存します。

MSYS2 UCRT64で、第6章の作業フォルダへ移動してください。

cd /c/c-db/chap06

簡潔なコンパイルコマンドは、次のとおりです。

gcc book_database_load.c -o book_database_load.exe $(pkg-config --cflags --libs gtk4 sqlite3)

C17と警告のオプションを指定する場合は、次のようにします。

gcc -std=c17 -Wall -Wextra book_database_load.c -o book_database_load.exe $(pkg-config --cflags --libs gtk4 sqlite3)

コンパイルに成功したら実行します。

./book_database_load.exe

接続先は、引き続きC:\c-db\chap06\books.dbです。

実行結果の例

確認用の2件だけが保存されている場合、ターミナルには次のように表示されます。

ID | タイトル | 著者名
1 | C言語入門 | 山田太郎
2 | SQLite入門 | 鈴木花子
読み込み件数:2件

GUIのメッセージ欄には、次の文字が表示されます。

2件の書籍を読み込みました。
内容はターミナルで確認できます。

登録・変更・削除ボタンは、引き続き無効です。クリアボタンは入力欄を空にしますが、読み込んだ書籍データやデータベースの内容は削除しません。

SELECTで取得する列と順番を決める

ここからは、読み込み処理の要点を抜き出して確認します。

load_booksで使用するSQLは、次のとおりです。

const char *sql =
    "SELECT id, title, author "
    "FROM books "
    "ORDER BY id;";

取得する列は、id、title、authorの3つです。

SQLの部分役割
SELECT id, title, author取得する列を指定する
FROM books読み込み元のテーブルを指定する
ORDER BY idIDの昇順に並べる

今回は検索条件を指定せず、すべての書籍を読み込みます。そのため、プレースホルダーへの値の設定は必要ありません。

取得する列番号は0から始まる

取得した値を取り出すときは、SELECTで指定した順番に列番号を付けます。

列番号対応する列
0id
1title
2author

この番号は、booksテーブルの定義そのものではなく、今回のSELECT結果の列順に対応しています。

sqlite3_stepで1行ずつ取得する

SQLを準備したら、sqlite3_stepを繰り返し呼び出します。

while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
    sqlite3_int64 id = sqlite3_column_int64(stmt, 0);

    const unsigned char *title =
        sqlite3_column_text(stmt, 1);

    if (title == NULL && sqlite3_errcode(db) == SQLITE_NOMEM) {
        rc = SQLITE_NOMEM;
        break;
    }

    const unsigned char *author =
        sqlite3_column_text(stmt, 2);

    if (author == NULL && sqlite3_errcode(db) == SQLITE_NOMEM) {
        rc = SQLITE_NOMEM;
        break;
    }

    Book *book = g_new0(Book, 1);

    book->id = id;
    book->title = g_strdup(
        title != NULL ? (const char *)title : ""
    );
    book->author = g_strdup(
        author != NULL ? (const char *)author : ""
    );

    g_ptr_array_add(books, book);
}

sqlite3_stepの結果によって、処理を進めます。

戻り値意味今回の対応
SQLITE_ROW1行分の結果を取得できた値を取り出して保存する
SQLITE_DONE結果の取得が完了した後片付けへ進む
その他エラーなどが発生した読み込み失敗として扱う

SQLITE_NOMEM は、SQLiteが処理に必要なメモリを確保できなかったことを示すエラーコードです。

SQLITE_ROWは、すべての読み込みが終わったという意味ではありません。次の行を取得するために、再びsqlite3_stepを呼び出します。

また、sqlite3_column_int64やsqlite3_column_textで現在の行を取得するのは、sqlite3_stepがSQLITE_ROWを返したあとです。

図2:sqlite3_stepで結果を1行ずつ読む

この図から分かること

sqlite3_stepがSQLITE_ROWを返すたびに、現在の1行を処理します。

繰り返しを抜けたあとも戻り値を確認し、正常に最後まで読めたのか、途中で失敗したのかを判断します。

文字列はコピーして保存する

今回の読み込み処理で特に大切なのは、文字列の扱いです。

sqlite3_column_textから受け取るポインタは、SQLiteが管理している文字列を指しています。

このポインタは、次のsqlite3_stepやsqlite3_finalizeなどで無効になるため、そのままBookに保存してあとから使うことはできません。

そこで、g_strdupで文字列をコピーしています。

book->title = g_strdup(
    title != NULL ? (const char *)title : ""
);

book->author = g_strdup(
    author != NULL ? (const char *)author : ""
);

g_strdupは、文字列のコピーを新しいメモリ領域に作ります。

これにより、SQLiteのステートメントを破棄したあとでも、Bookに保存したタイトルと著者名を利用できます。

ポインタの型を変えるだけではコピーにならない

コード中の次の部分は、ポインタをconst char *として扱うためのキャストです。

(const char *)title

キャストそのものは、文字列のコピーを作りません。

実際にコピーしているのは、その外側のg_strdupです。

NULLとメモリ不足を区別する

sqlite3_column_textは、SQLの値がNULLの場合にも、文字列取得時にメモリ不足が発生した場合にも、NULLポインタを返すことがあります。

そのため、NULLだった場合は、直後にsqlite3_errcodeでSQLITE_NOMEMかどうかを確認しています。(sqlite.org)

今回のbooksテーブルにはNOT NULL制約がありますが、既存データへの対応として、SQLのNULLは表示用の空文字列へ置き換えるようにしています。

読み込んだBookを配列へ追加する

1件分の構造体は、次のコードで確保します。

Book *book = g_new0(Book, 1);

g_new0は、指定した型の領域を確保し、ゼロで初期化するGLibのマクロです。

値を設定したあと、配列へ追加します。

g_ptr_array_add(books, book);

ここで追加しているのは、Book構造体そのもののコピーではなく、Bookへのポインタです。

今回使用する配列関連の操作を整理しておきましょう。

操作用途
g_ptr_array_new_with_free_func要素の解放方法を指定して配列を作る
g_ptr_array_addポインタを追加する
g_ptr_array_index指定位置のポインタを取り出す
books->len保存されている要素数を確認する
g_ptr_array_unref配列の参照を手放す

GPtrArrayを使うことで、書籍を何件取得するかに合わせて、要素を追加できます。

読み込み成功と0件と失敗を区別する

load_booksは、次のルールで結果を渡します。

結果戻り値渡される配列
1件以上を正常に取得SQLITE_OK書籍を含む配列
0件で正常終了SQLITE_OK要素数0の配列
読み込み失敗エラーコードNULL

関数の先頭では、出力先をNULLにしています。

*out_books = NULL;

そして、最後まで正常に読み込めた場合だけ、作成した配列を渡します。

*out_books = books;

return SQLITE_OK;

0件の場合でも、空の配列を正常な結果として返します。

書籍が0件であることは、読み込みエラーではありません。

配列を受け取るためにポインタのアドレスを渡す

呼び出し元は、次のようになっています。

rc = load_books(data->db, &data->books);
引数役割
data->db読み込みに使用する接続
&data->books作成した配列へのポインタを受け取る場所

open_databaseで接続を受け取ったときと同じように、ポインタ変数のアドレスを渡しています。

このload_booksは、新しい配列を返す関数です。すでに配列が保存されている場所へ直接上書きすると、以前の配列を解放できなくなるため、今回は初期値がNULLのdata->booksへ一度だけ読み込みます。

再読み込み時の入れ替えは、後の記事で整理していきます。

途中で失敗した場合は取得済みのデータも片付ける

ループを抜けたあと、ステートメントを破棄します。

int finalize_rc = sqlite3_finalize(stmt);

if (rc == SQLITE_DONE) {
    rc = finalize_rc;
}

SQLITE_DONEで正常に取得を終えた場合は、sqlite3_finalizeの結果も確認します。sqlite3_finalizeは、ステートメントを破棄する関数です。

finalize_rc には、sqlite3_finalize(stmt) が返した結果コードが入ります。

sqlite3_finalize() は、準備済みSQL文 stmt を破棄する関数です。直近のSQL実行でエラーがなければ SQLITE_OK、エラーがあれば対応するエラーコードを返します。

rc の状態処理
SQLITE_DONE(行の読み込みが正常終了)finalize_rc を最終結果として rc に入れる
SQLITE_NOMEM などのエラーrc を書き換えず、そのエラーを残す

読み込みや後片付けに失敗した場合は、途中まで作成した配列を解放します。

if (rc != SQLITE_OK) {
    g_ptr_array_unref(books);
    return rc;
}

これにより、途中まで取得した内容を、完全な読み込み結果として呼び出し元へ渡さずに済みます。

例えば、10件あるうちの3件を取得したところで失敗した場合、その3件を「正常に読み込んだ全件」として扱うことはありません。

配列と書籍情報の解放を結び付ける

配列を作るときには、free_bookを指定しています。

GPtrArray *books =
    g_ptr_array_new_with_free_func(free_book);

free_bookは、1件分の書籍を解放する関数です。

static void free_book(gpointer data)
{
    Book *book = data;

    g_free(book->title);
    g_free(book->author);
    g_free(book);
}

解放する順番は、次のとおりです。

  1. コピーしたタイトルを解放する。
  2. コピーした著者名を解放する。
  3. Book構造体を解放する。

今回の配列は他の場所で参照カウントを増やしていないため、g_ptr_array_unrefで配列を解放するときに、各要素へfree_bookが呼ばれます。

図3:コピーした書籍情報を終了時に解放する

この図から分かること

SQLiteから取得した文字列をコピーすると、読み込み処理が終わったあとも利用できます。

その代わり、コピーした文字列とBookを、使い終わったときに解放する必要があります。

呼び出し元で書籍の内容を確認する

load_booksが成功したら、呼び出し元で内容を表示します。

print_loaded_books(data->books);

print_loaded_booksでは、配列からBookを取り出しています。

for (guint i = 0; i < books->len; i++) {
    const Book *book = g_ptr_array_index(books, i);

    g_print(
        "%lld | %s | %s\n",
        (long long)book->id,
        book->title,
        book->author
    );
}

配列から取り出した値は、追加したときと同じBookへのポインタです。

ここでは表示だけを行うため、const Book *として受け取っています。

GUI側には、読み込んだ件数を渡します。

create_main_window(
    app,
    &data->widgets,
    data->books->len
);

読み込みと表示を分けているため、次の記事ではBookの内容を使ってGUIの一覧を作れます。

0件の場合の表示も確認する

確認用データを追加する前など、booksテーブルが空の場合は、load_booksは正常終了し、配列の要素数が0になります。

ターミナルには、次のように表示されます。

登録されている書籍はありません。

GUIには、次のように表示されます。

読み込みは正常に完了しました。
登録されている書籍はありません。

一方、SQLの実行などに失敗した場合は、書籍を読み込めませんでしたというエラー画面になります。

0件と失敗を分けることで、利用者へ正しい状況を伝えられます。

今回の読み込みは起動時に一度だけ行う

今回のプログラムは、起動時にデータベースの内容を読み込み、メモリへ保持します。

その後、別のプログラムでbooks.dbを変更しても、保持しているBookの内容は自動では変わりません。

確認用データを追加したあとに内容を確認する場合は、アプリを終了して再起動してください。

また、メモリ上のBookを変更するだけでは、SQLiteへのUPDATEにはなりません。読み込んだデータと、データベースへの保存処理は別のものです。

次の記事では「GTK 4の画面へ書籍一覧を表示する」をテーマに、今回取得したBookを使って、ID・タイトル・著者名をウィンドウへ表示していきましょう。

Follow me!