C言語DB入門|コンソール版書籍管理アプリを完成させる

C言語DB入門|コンソール版書籍管理アプリを完成させる

この記事では、これまで作成した登録・一覧表示・変更・削除の機能をまとめ、メニューから操作できるコンソール版書籍管理アプリを完成させます。

機能ごとにプログラムを起動する段階から、1つのアプリで操作を繰り返せる段階へ進みましょう。

  • メニュー番号に応じてCRUD機能を呼び出す
  • 操作後にメニューへ戻る
  • 入力ミスを確認して再入力する
  • 対象がない場合やキャンセルを扱う
  • 入力の終端やエラーで適切に終了する
  • 再起動後も保存した書籍を利用する

完成コードは、1つのbook_manager.cにまとめます。これまでの記事を振り返りながら、各処理がどのようにつながるのかを確認してください。

完成するアプリの動作

アプリを起動すると、次のメニューを表示します。

書籍管理アプリ

1. 書籍を登録
2. 書籍を一覧表示
3. 書籍を変更
4. 書籍を削除
0. 終了

番号を入力してください:

利用者が番号を入力すると、対応する関数を呼び出します。

番号機能呼び出す関数
1書籍を登録add_book
2書籍を一覧表示list_books
3書籍を変更update_book
4書籍を削除delete_book
0終了メニューの繰り返しを終了する

登録・一覧表示・変更・削除が終わると、再びメニューを表示します。

データベース接続は起動時に開き、操作中は共有します。終了するときに、その接続を閉じます。

図1:メニューを中心にCRUDの操作を繰り返す

この図から分かること

各機能は処理を終えると、メニューへ戻ります。

利用者はアプリを起動し直さずに、登録後の一覧確認や、変更後の再確認を続けられます。

ファイルと保存場所を確認する

作業場所は、これまでと同じC:\c-db\chap04です。

ファイル名役割
book_manager.c完成版のソースファイル
book_manager.exe完成版の実行ファイル
books.db書籍を保存するデータベース

chap04内にbooks.dbがあれば、そのデータを引き続き使用します。存在しなければ、新しいデータベースとbooksテーブルを作成します。

テーブルの構造は、これまでと同じです。

列名定義内容
idINTEGER PRIMARY KEY AUTOINCREMENT書籍を識別するID
titleTEXT NOT NULLタイトル
authorTEXT NOT NULL著者名

CREATE TABLE IF NOT EXISTSは、既存のテーブルを今回の定義へ変更するものではありません。本教材で作成した構造のbooksテーブルを使用してください。

入力とエラーの扱いを統一する

完成版では、次の方針にします。

状況動作
メニュー番号が不正番号を再入力する
タイトルや著者名が空同じ項目を再入力する
入力が長すぎる残りを読み捨てて再入力する
IDの形式が不正IDを再入力する
指定したIDが存在しない案内してメニューへ戻る
削除をキャンセルしたメニューへ戻る
入力の終端に達した接続を閉じて正常終了する
SQLiteや入力の読み取りに失敗した詳細を表示して終了する

削除の確認入力が長すぎた場合は、削除をキャンセルします。

また、今回の完成版では、SQLiteのエラー発生後はアプリを終了する方針に統一します。エラーの種類ごとに継続可能かを判断する仕組みは、必要になった段階で拡張できます。

完成版のプログラムを作成する

VS Codeでbook_manager.cを作成し、UTF-8で保存してください。

コードは長めですが、入力、SQLiteの共通処理、CRUD、メニューの順に並べています。まず全体を保存し、動かしてから各部分を確認しましょう。

メニューから登録・一覧表示・変更・削除を行う書籍管理プログラム

ファイル名: book_manager.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <sqlite3.h>

#define TEXT_CAPACITY 256

typedef enum {
    INPUT_OK, INPUT_INVALID, INPUT_END, INPUT_ERROR
} InputResult;

typedef enum {
    ACTION_OK, ACTION_END, ACTION_ERROR
} ActionResult;

static void report_sqlite_error(sqlite3 *db,
                                const char *action, int rc)
{
    const char *message = db != NULL
        ? sqlite3_errmsg(db) : sqlite3_errstr(rc);

    fprintf(stderr, "%sに失敗しました。\n", action);
    fprintf(stderr, "SQLiteコード:%d\n", rc);
    fprintf(stderr, "詳細:%s\n",
            message != NULL ? message : "詳細を取得できませんでした。");
}

/* 改行を除く1行を読む。空文字も受け付ける。 */
static InputResult read_line(const char *prompt,
                             char *buffer, int capacity)
{
    int ch;
    printf("%s", prompt);
    fflush(stdout);

    if (fgets(buffer, capacity, stdin) == NULL) {
        if (ferror(stdin)) {
            fprintf(stderr, "入力の読み取りに失敗しました。\n");
            return INPUT_ERROR;
        }
        return INPUT_END;
    }

    if (ferror(stdin)) {
        fprintf(stderr, "入力の読み取りに失敗しました。\n");
        return INPUT_ERROR;
    }

    if (strchr(buffer, '\n') == NULL) {
        ch = getchar();
        if (ch != '\n' && ch != EOF) {
            while ((ch = getchar()) != '\n' && ch != EOF) {
                /* 長すぎた入力の残りを読み捨てる。 */
            }
            if (ferror(stdin)) {
                fprintf(stderr, "入力の読み取りに失敗しました。\n");
                return INPUT_ERROR;
            }
            printf("入力が長すぎます。%dバイト以内で入力してください。\n",
                   capacity - 1);
            return INPUT_INVALID;
        }
        if (ferror(stdin)) {
            fprintf(stderr, "入力の読み取りに失敗しました。\n");
            return INPUT_ERROR;
        }
    }

    buffer[strcspn(buffer, "\r\n")] = '\0';
    return INPUT_OK;
}

static InputResult read_required_text(const char *prompt,
                                      char *buffer, int capacity)
{
    while (1) {
        InputResult result = read_line(prompt, buffer, capacity);
        if (result == INPUT_INVALID) {
            continue;
        }
        if (result != INPUT_OK) {
            return result;
        }
        if (buffer[0] == '\0' ||
            strspn(buffer, " \t") == strlen(buffer)) {
            printf("文字を入力してください。\n");
            continue;
        }
        return INPUT_OK;
    }
}

static InputResult read_number(const char *prompt,
                               long long min_value, long long max_value,
                               sqlite3_int64 *number)
{
    char input[64];
    while (1) {
        InputResult result =
            read_required_text(prompt, input, sizeof input);
        if (result != INPUT_OK) {
            return result;
        }

        char *end = NULL;
        errno = 0;
        long long value = strtoll(input, &end, 10);

        if (end == input || errno == ERANGE) {
            printf("範囲内の整数を入力してください。\n");
            continue;
        }
        while (*end == ' ' || *end == '\t') {
            end++;
        }
        if (*end != '\0' || value < min_value || value > max_value) {
            printf("入力できる範囲の整数を入力してください。\n");
            continue;
        }
        *number = (sqlite3_int64)value;
        return INPUT_OK;
    }
}

static ActionResult input_action(InputResult result)
{
    return result == INPUT_END ? ACTION_END : ACTION_ERROR;
}

static int finish_statement(sqlite3 *db, sqlite3_stmt *stmt)
{
    int rc = sqlite3_finalize(stmt);
    if (rc != SQLITE_OK) {
        report_sqlite_error(db, "SQLの終了処理", rc);
        return 0;
    }
    return 1;
}

/* 準備・バインド済みの書き込みSQLを実行し、stmtを必ず解放する。 */
static int execute_change(sqlite3 *db, sqlite3_stmt *stmt, int *changed)
{
    int rc = sqlite3_step(stmt);
    if (rc != SQLITE_DONE) {
        report_sqlite_error(db, "データの書き込み", rc);
        sqlite3_finalize(stmt);
        return 0;
    }
    *changed = sqlite3_changes(db);
    return finish_statement(db, stmt);
}

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"
        ");";

    int rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
    if (rc != SQLITE_OK) {
        report_sqlite_error(db, "テーブルの準備", rc);
        return 0;
    }
    return 1;
}

/* 見つかれば表示して1、対象なしは0、エラーは-1を返す。 */
static int show_book(sqlite3 *db, sqlite3_int64 id)
{
    const char *sql = "SELECT title, author FROM books WHERE id = ?;";
    sqlite3_stmt *stmt = NULL;
    int found = 0;
    int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    if (rc == SQLITE_OK) {
        rc = sqlite3_bind_int64(stmt, 1, id);
    }
    if (rc != SQLITE_OK) {
        report_sqlite_error(db, "書籍確認の準備", rc);
        sqlite3_finalize(stmt);
        return -1;
    }

    rc = sqlite3_step(stmt);
    if (rc == SQLITE_ROW) {
        const unsigned char *title = sqlite3_column_text(stmt, 0);
        if (title == NULL) {
            fprintf(stderr, "タイトルを取得できませんでした。\n");
            sqlite3_finalize(stmt);
            return -1;
        }
        const unsigned char *author = sqlite3_column_text(stmt, 1);
        if (author == NULL) {
            fprintf(stderr, "著者名を取得できませんでした。\n");
            sqlite3_finalize(stmt);
            return -1;
        }
        printf("\nID:%lld\nタイトル:%s\n著者名:%s\n\n",
               (long long)id, (const char *)title, (const char *)author);
        found = 1;
    } else if (rc != SQLITE_DONE) {
        report_sqlite_error(db, "書籍の取得", rc);
        sqlite3_finalize(stmt);
        return -1;
    }

    if (!finish_statement(db, stmt)) {
        return -1;
    }
    return found;
}

static ActionResult add_book(sqlite3 *db)
{
    char title[TEXT_CAPACITY];
    char author[TEXT_CAPACITY];
    InputResult result = read_required_text("タイトル:", title, sizeof title);
    if (result != INPUT_OK) {
        return input_action(result);
    }
    result = read_required_text("著者名:", author, sizeof author);
    if (result != INPUT_OK) {
        return input_action(result);
    }

    const char *sql = "INSERT INTO books (title, author) VALUES (?, ?);";
    sqlite3_stmt *stmt = NULL;
    int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    if (rc == SQLITE_OK) {
        rc = sqlite3_bind_text(stmt, 1, title, -1, SQLITE_TRANSIENT);
    }
    if (rc == SQLITE_OK) {
        rc = sqlite3_bind_text(stmt, 2, author, -1, SQLITE_TRANSIENT);
    }
    if (rc != SQLITE_OK) {
        report_sqlite_error(db, "登録の準備", rc);
        sqlite3_finalize(stmt);
        return ACTION_ERROR;
    }

    int changed;
    if (!execute_change(db, stmt, &changed)) {
        return ACTION_ERROR;
    }
    printf("書籍を登録しました。\n");
    return ACTION_OK;
}

static ActionResult list_books(sqlite3 *db)
{
    const char *sql = "SELECT id, title, author FROM books ORDER BY id;";
    sqlite3_stmt *stmt = NULL;
    size_t count = 0;
    int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    if (rc != SQLITE_OK) {
        report_sqlite_error(db, "一覧SQLの準備", rc);
        sqlite3_finalize(stmt);
        return ACTION_ERROR;
    }

    printf("\n書籍一覧\n");
    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) {
            fprintf(stderr, "タイトルを取得できませんでした。\n");
            sqlite3_finalize(stmt);
            return ACTION_ERROR;
        }
        const unsigned char *author = sqlite3_column_text(stmt, 2);
        if (author == NULL) {
            fprintf(stderr, "著者名を取得できませんでした。\n");
            sqlite3_finalize(stmt);
            return ACTION_ERROR;
        }
        if (count == 0) {
            printf("ID | タイトル | 著者名\n");
        }
        printf("%lld | %s | %s\n", (long long)id,
               (const char *)title, (const char *)author);
        count++;
    }
    if (rc != SQLITE_DONE) {
        report_sqlite_error(db, "一覧の取得", rc);
        sqlite3_finalize(stmt);
        return ACTION_ERROR;
    }
    if (!finish_statement(db, stmt)) {
        return ACTION_ERROR;
    }
    if (count == 0) {
        printf("書籍はまだ登録されていません。\n");
    } else {
        printf("\n%zu件の書籍があります。\n", count);
    }
    return ACTION_OK;
}

static ActionResult update_book(sqlite3 *db)
{
    sqlite3_int64 id;
    char title[TEXT_CAPACITY];
    char author[TEXT_CAPACITY];
    InputResult result =
        read_number("変更する書籍のID:", 1, LLONG_MAX, &id);
    if (result != INPUT_OK) {
        return input_action(result);
    }

    int found = show_book(db, id);
    if (found < 0) {
        return ACTION_ERROR;
    }
    if (found == 0) {
        printf("指定したIDの書籍は見つかりませんでした。\n");
        return ACTION_OK;
    }

    result = read_required_text("新しいタイトル:", title, sizeof title);
    if (result != INPUT_OK) {
        return input_action(result);
    }
    result = read_required_text("新しい著者名:", author, sizeof author);
    if (result != INPUT_OK) {
        return input_action(result);
    }

    const char *sql =
        "UPDATE books SET title = ?, author = ? WHERE id = ?;";
    sqlite3_stmt *stmt = NULL;
    int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    if (rc == SQLITE_OK) {
        rc = sqlite3_bind_text(stmt, 1, title, -1, SQLITE_TRANSIENT);
    }
    if (rc == SQLITE_OK) {
        rc = sqlite3_bind_text(stmt, 2, author, -1, SQLITE_TRANSIENT);
    }
    if (rc == SQLITE_OK) {
        rc = sqlite3_bind_int64(stmt, 3, id);
    }
    if (rc != SQLITE_OK) {
        report_sqlite_error(db, "変更の準備", rc);
        sqlite3_finalize(stmt);
        return ACTION_ERROR;
    }

    int changed;
    if (!execute_change(db, stmt, &changed)) {
        return ACTION_ERROR;
    }
    printf("%s\n", changed == 0
           ? "変更対象の書籍が見つかりませんでした。"
           : "書籍を変更しました。");
    return ACTION_OK;
}

static ActionResult delete_book(sqlite3 *db)
{
    sqlite3_int64 id;
    char answer[16];
    InputResult result =
        read_number("削除する書籍のID:", 1, LLONG_MAX, &id);
    if (result != INPUT_OK) {
        return input_action(result);
    }

    int found = show_book(db, id);
    if (found < 0) {
        return ACTION_ERROR;
    }
    if (found == 0) {
        printf("指定したIDの書籍は見つかりませんでした。\n");
        return ACTION_OK;
    }

    result = read_line("この書籍を削除しますか?(yで削除):",
                       answer, sizeof answer);
    if (result == INPUT_END || result == INPUT_ERROR) {
        return input_action(result);
    }
    if (result == INPUT_OK && feof(stdin)) {
        /* 確認入力が改行前に終わった場合は削除しない。 */
        return ACTION_END;
    }
    if (result == INPUT_INVALID || strcmp(answer, "y") != 0) {
        printf("削除をキャンセルしました。\n");
        return ACTION_OK;
    }

    const char *sql = "DELETE FROM books WHERE id = ?;";
    sqlite3_stmt *stmt = NULL;
    int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    if (rc == SQLITE_OK) {
        rc = sqlite3_bind_int64(stmt, 1, id);
    }
    if (rc != SQLITE_OK) {
        report_sqlite_error(db, "削除の準備", rc);
        sqlite3_finalize(stmt);
        return ACTION_ERROR;
    }

    int changed;
    if (!execute_change(db, stmt, &changed)) {
        return ACTION_ERROR;
    }
    printf("%s\n", changed == 0
           ? "削除対象の書籍が見つかりませんでした。"
           : "書籍を削除しました。");
    return ACTION_OK;
}

static void show_menu(void)
{
    printf("\n書籍管理アプリ\n\n");
    printf("1. 書籍を登録\n");
    printf("2. 書籍を一覧表示\n");
    printf("3. 書籍を変更\n");
    printf("4. 書籍を削除\n");
    printf("0. 終了\n\n");
}

int main(void)
{
    sqlite3 *db = NULL;
    int exit_status = EXIT_SUCCESS;
    int rc = sqlite3_open("books.db", &db);
    if (rc != SQLITE_OK) {
        report_sqlite_error(db, "データベースを開く処理", rc);
        if (db != NULL) {
            sqlite3_close(db);
        }
        return EXIT_FAILURE;
    }

    if (!initialize_database(db)) {
        exit_status = EXIT_FAILURE;
    } else {
        while (1) {
            sqlite3_int64 choice;
            show_menu();
            InputResult input =
                read_number("番号を入力してください:", 0, 4, &choice);

            if (input == INPUT_END) {
                printf("\n入力が終了したため、終了します。\n");
                break;
            }
            if (input == INPUT_ERROR) {
                exit_status = EXIT_FAILURE;
                break;
            }
            if (choice == 0) {
                break;
            }

            ActionResult action = ACTION_OK;
            switch (choice) {
            case 1: action = add_book(db); break;
            case 2: action = list_books(db); break;
            case 3: action = update_book(db); break;
            case 4: action = delete_book(db); break;
            }

            if (action == ACTION_END) {
                printf("\n入力が終了したため、終了します。\n");
                break;
            }
            if (action == ACTION_ERROR) {
                fprintf(stderr, "処理を続けられないため、終了します。\n");
                exit_status = EXIT_FAILURE;
                break;
            }
        }
    }

    rc = sqlite3_close(db);
    if (rc != SQLITE_OK) {
        report_sqlite_error(db, "データベース接続を閉じる処理", rc);
        return EXIT_FAILURE;
    }
    if (exit_status == EXIT_SUCCESS) {
        printf("アプリを終了しました。\n");
    }
    return exit_status;
}

mainがアプリ全体の進行を管理する

mainの役割は、大きく分けて3つです。

  1. データベースを開き、テーブルを準備する。
  2. メニューを繰り返し、選ばれた関数を呼び出す。
  3. 終了時にデータベース接続を閉じる。

メニュー番号はread_numberで0〜4に制限しています。そのため、switchへ進む時点では、有効な番号を受け取れています。

switchのbreakとwhileのbreakは役割が異なる

switch内の各caseにあるbreakは、switchを抜けるためのものです。メニューの繰り返しは終了しません。

一方、0が選ばれた場合やACTION_ENDを受け取った場合のbreakは、whileを抜けます。

breakの場所抜ける対象
caseの末尾switch
choiceが0の場合メニューのwhile
ACTION_ENDやACTION_ERRORの場合メニューのwhile

この違いによって、通常の操作後はメニューへ戻り、終了条件に合ったときだけアプリを終了できます。

各機能の戻り値をそろえる

CRUDの関数は、ActionResultを返します。

戻り値意味mainの動作
ACTION_OK操作を終え、続行できるメニューを表示する
ACTION_END入力の終端に達した正常終了へ進む
ACTION_ERROR処理に失敗したエラー終了へ進む

ACTION_OKには、登録成功などに加え、対象なしや削除のキャンセルも含めています。

ここでの戻り値は「データが変更されたか」ではなく、アプリを続けられるかどうかを表します。具体的な結果は、それぞれの関数内で表示します。

図2:各機能の結果をmainへ返す

この図から分かること

各機能は、自分の処理を終えた結果をmainへ返します。

mainがその結果を確認することで、メニューへ戻るか、後片付けして終了するかを一か所で管理できます。

共通処理をまとめた部分を確認する

完成版では、複数の機能で使う処理を共通の関数へまとめています。

関数名共通化した処理
read_line1行の読み取りと長さの確認
read_required_text空文字などを拒否して再入力
read_number整数への変換と範囲の確認
show_bookIDで書籍を取得して表示
report_sqlite_errorSQLiteのエラー表示
finish_statementステートメントの終了処理
execute_change書き込みSQLの実行、件数取得、後片付け

メニューとIDで同じ数値入力関数を使う

read_numberには、受け付ける最小値と最大値を渡します。

用途最小値最大値
メニュー番号04
書籍ID1LLONG_MAX

LLONG_MAXは、long longで扱える最大値です。本教材のUCRT64環境では、64ビット符号付き整数の範囲になります。

同じ関数を使いながら、メニューでは0を終了として受け付け、IDでは0を拒否できます。

空欄を許す入力と必須入力を分ける

read_lineは、空文字も受け付けます。削除の確認でEnterキーだけを押し、キャンセルできるようにするためです。

タイトルや著者名ではread_required_textを使い、空文字と半角スペース・タブだけの入力を拒否します。

タイトルと著者名の上限は、それぞれ255バイトです。日本語を含む場合は、文字数とバイト数が異なる点に注意してください。

SQLの実行と後片付けを共通化する

add_book、update_book、delete_bookは、それぞれのSQLを準備して値をバインドした後、execute_changeを呼び出します。

execute_changeは、次の処理を担当します。

  1. sqlite3_stepで実行する。
  2. SQLITE_DONEを確認する。
  3. sqlite3_changesで対象件数を取得する。
  4. ステートメントを解放する。

sqlite3_changesは、同じ接続で直前に完了したINSERT・UPDATE・DELETEが直接変更した行数を返します。

execute_changeは、成功・失敗のどちらでも、渡されたステートメントを解放します。呼び出し元で重ねてsqlite3_finalizeを呼ばないようにしています。

また、sqlite3_finalizeがエラーを返した場合でも、ステートメントは再利用できません。同じポインターを再び解放してはいけません。

コンパイルして起動する

MSYS2 UCRT64で、作業場所へ移動します。

cd /c/c-db/chap04

短いコンパイルコマンドはこちらです。

gcc book_manager.c -o book_manager.exe -lsqlite3

C17と警告オプションを指定する場合は、次のコマンドを使用します。

gcc -std=c17 -Wall -Wextra book_manager.c -o book_manager.exe -lsqlite3

どちらか一方でコンパイルし、成功したら起動してください。

./book_manager.exe

コンパイル対象はbook_manager.cだけです。以前作成したadd_book.cなどは、それぞれmainを持つ独立したプログラムなので、今回のコマンドへ一緒に指定しません。

登録して一覧を確認する

以下は、新しいデータベースに1件登録する場合の表示例です。操作ごとの画面部分を掲載しています。各操作の後には、同じメニューが再び表示されます。

既存のbooks.dbを使っている場合は、実際のIDや件数に読み替えてください。

書籍を登録したときの実行結果の例

メニューで1を選びます。

番号を入力してください:1
タイトル:はじめてのC言語
著者名:佐藤花子
書籍を登録しました。

一覧表示したときの実行結果の例

続けて、メニューで2を選びます。

番号を入力してください:2

書籍一覧
ID | タイトル | 著者名
1 | はじめてのC言語 | 佐藤花子

1件の書籍があります。

登録から一覧表示まで、同じアプリの中で操作できました。

登録した書籍を変更する

メニューで3を選び、一覧で確認したIDを入力します。

今回は変更前の情報も表示するため、対象を確認してから新しい内容を入力できます。

変更したときの実行結果の例

番号を入力してください:3
変更する書籍のID:1

ID:1
タイトル:はじめてのC言語
著者名:佐藤花子

新しいタイトル:はじめてのC言語 改訂版
新しい著者名:佐藤花子
書籍を変更しました。

タイトルと著者名は両方入力します。変更しない項目には、現在と同じ内容を入力してください。

同じ値を入力した場合も、今回のUPDATEでは対象の行が更新件数に含まれます。完了メッセージは、更新処理が実行されたことを示しています。

終了してから、保存された内容を確認する

メニューで0を選びます。

番号を入力してください:0
アプリを終了しました。

もう一度起動してください。

./book_manager.exe

メニューで2を選ぶと、変更後の書籍を確認できます。

番号を入力してください:2

書籍一覧
ID | タイトル | 著者名
1 | はじめてのC言語 改訂版 | 佐藤花子

1件の書籍があります。

今回のプログラムでは、成功した書き込みがその都度、自動的に確定されます。保存先はbooks.dbなので、プログラムを終了してもデータが残ります。

図3:終了後も同じデータベースから学習を続けられる

この図から分かること

プログラムを終了しても、書籍データはbooks.dbに残ります。

再起動後は同じファイルを開き、前回までの登録・変更内容を引き続き利用できます。

削除のキャンセルと実行を確認する

メニューで4を選び、対象のIDを入力します。

キャンセルしたときの実行結果の例

番号を入力してください:4
削除する書籍のID:1

ID:1
タイトル:はじめてのC言語 改訂版
著者名:佐藤花子

この書籍を削除しますか?(yで削除):n
削除をキャンセルしました。

キャンセルすると、書籍を残してメニューへ戻ります。

削除したときの実行結果の例

削除してよければ、再び4を選び、小文字のyとEnterキーで確定します。

番号を入力してください:4
削除する書籍のID:1

ID:1
タイトル:はじめてのC言語 改訂版
著者名:佐藤花子

この書籍を削除しますか?(yで削除):y
書籍を削除しました。

この1件だけが登録されていた場合、一覧表示は次のようになります。

番号を入力してください:2

書籍一覧
書籍はまだ登録されていません。

削除しても、booksテーブルやbooks.dbは残ります。再び登録機能を使って、新しい書籍を追加できます。

入力ミスから復帰できることを確認する

通常の操作に加え、入力ミスも試しておきましょう。

メニュー番号が不正だった場合

番号を入力してください:9
入力できる範囲の整数を入力してください。
番号を入力してください:2

タイトルが空だった場合

番号を入力してください:1
タイトル:
文字を入力してください。
タイトル:SQLite入門
著者名:田中太郎
書籍を登録しました。

指定したIDがなかった場合

IDが999の書籍が存在しない場合の例です。

番号を入力してください:3
変更する書籍のID:999
指定したIDの書籍は見つかりませんでした。

この後はメニューへ戻ります。

タイトルや著者名の入力途中で入力終端になった場合は、登録・変更のSQLを実行せずに終了します。入力内容をすべて受け取ってから書き込む構成になっているためです。

保存した書籍が見つからないときは

books.dbは相対パスで指定しています。基準になるのは、実行時の作業ディレクトリです。

今回のアプリは、次の順番で起動してください。

cd /c/c-db/chap04
./book_manager.exe

別のディレクトリから起動すると、その場所に別のbooks.dbを作成する場合があります。

また、前の記事のbook_error_handling.cは読み取り専用の確認プログラムでした。完成版では登録・変更・削除を行うため、sqlite3_openで書き込みに対応した接続を開いています。

完成版の動作を一通り確かめる

最後に、次の操作を確認しておくと、各機能のつながりを確かめられます。

確認する操作期待する動作
書籍を登録する新しい行が追加される
一覧を表示するID順に書籍が表示される
タイトルや著者名を変更する指定したIDの内容が変わる
削除をキャンセルする書籍が残る
削除を確定する指定したIDの行がなくなる
存在しないIDを指定する案内してメニューへ戻る
不正な番号を入力する再入力できる
終了して再起動する保存した内容を利用できる
入力の終端に達する繰り返し続けずに終了する

今回の完成版は、1人でコンソールから書籍を管理するための基本的な構成です。

今後GUIへ発展させるときも、SQLの準備、値のバインド、実行、後片付けという流れは共通です。今回整理した入力・表示・データベース操作の役割を手掛かりに、ボタンや入力欄から操作するアプリへつなげていきましょう。

Follow me!