Python標準ライブラリsqlite3を徹底解説! 🐍💾

プログラミング

軽量データベースSQLiteをPythonで自在に操る方法

はじめに:SQLiteとsqlite3モジュール

SQLiteは、サーバープロセスを必要としない、軽量なディスクベースのリレーショナルデータベース管理システム(RDBMS)です。データベース全体が単一のファイルとして保存されるため、セットアップが非常に簡単で、持ち運びにも便利です。小規模なアプリケーション、Webサイトのプロトタイピング、テスト、組み込みシステム、データ分析の中間ストレージなど、様々な場面で活躍します。😊

Pythonには、このSQLiteデータベースを操作するための標準ライブラリとしてsqlite3モジュールが組み込まれています。これは、Python 2.5以降で標準搭載されているため、追加のインストール作業なしにすぐに利用できます。sqlite3モジュールは、Python Database API Specification v2.0 (PEP 249)に準拠しており、PythonからSQLを使ってSQLiteデータベースを操作するための統一されたインターフェースを提供します。

このブログ記事では、sqlite3モジュールの基本的な使い方から、トランザクション管理、データ型の扱い、ベストプラクティスまで、幅広く解説していきます。

基本的な使い方:接続からCRUD操作まで

まず、SQLiteデータベースファイルに接続します。sqlite3.connect()関数を使用し、引数にデータベースファイルへのパスを指定します。ファイルが存在しない場合は、新しく作成されます。メモリ上に一時的なデータベースを作成したい場合は、ファイルパスとして':memory:'を指定します。


import sqlite3

# 'mydatabase.db'というファイルに接続(なければ作成)
conn = sqlite3.connect('mydatabase.db')

# メモリ上にデータベースを作成
# conn = sqlite3.connect(':memory:')

print("データベースに接続しました!🎉")
      

connect()関数はConnectionオブジェクトを返します。このオブジェクトを通じて、データベースに対する様々な操作を行います。

SQL文を実行し、結果を取得するためには、Cursorオブジェクトが必要です。Connectionオブジェクトのcursor()メソッドを呼び出して作成します。カーソルは、データベース内を移動し、データを操作するための中間的な役割を果たします。🖱️


# カーソルオブジェクトを作成
cursor = conn.cursor()
print("カーソルを作成しました!")
      

データを格納するためのテーブルを作成します。SQLのCREATE TABLE文をcursor.execute()メソッドの引数として渡します。IF NOT EXISTS句を使うと、同名のテーブルが既に存在する場合にエラーになるのを防げます。


# usersテーブルを作成 (存在しない場合のみ)
sql_create_table = """
CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE,
    age INTEGER
);
"""
cursor.execute(sql_create_table)
print("usersテーブルを作成しました (または既に存在します)。")
      
  • INTEGER PRIMARY KEY AUTOINCREMENT: 整数型の主キーで、自動的に連番が割り振られます。
  • TEXT NOT NULL: 文字列型で、NULL値を許可しません。
  • TEXT UNIQUE: 文字列型で、重複する値を許可しません。

テーブルに新しいデータを挿入するには、SQLのINSERT INTO文を使用します。SQLインジェクション攻撃を防ぐため、値を直接SQL文に埋め込むのではなく、プレースホルダ(?)を使用するのが非常に重要です。値はexecute()メソッドの第2引数としてタプルで渡します。


# データを挿入 (プレースホルダを使用)
sql_insert = "INSERT INTO users (name, email, age) VALUES (?, ?, ?);"
user_data = ('Alice Wonderland', 'alice@example.com', 30)

try:
    cursor.execute(sql_insert, user_data)
    print(f"データ {user_data} を挿入しました。")

    # 複数のデータを挿入 (executemany)
    more_users = [
        ('Bob The Builder', 'bob@example.com', 45),
        ('Charlie Chaplin', 'charlie@example.com', 55)
    ]
    cursor.executemany(sql_insert, more_users)
    print("複数のデータを挿入しました。")

    # 変更を確定 (コミット)
    conn.commit()
    print("変更をコミットしました。✅")

except sqlite3.IntegrityError as e:
    print(f"データの挿入に失敗しました (制約違反): {e}")
    conn.rollback() # エラーが発生した場合は変更を取り消す (ロールバック)
except sqlite3.Error as e:
    print(f"データベースエラーが発生しました: {e}")
    conn.rollback()

      

データの挿入、更新、削除などの変更操作を行った後は、connection.commit()を呼び出して変更をデータベースファイルに永続化する必要があります。エラーが発生した場合などはconnection.rollback()で変更を取り消すことができます。

複数のデータをまとめて挿入したい場合は、cursor.executemany()メソッドが便利です。リストやタプルのリストを第2引数に渡します。

テーブルからデータを取得するには、SQLのSELECT文を使用します。execute()でクエリを実行した後、以下のメソッドで結果を取得できます。

  • fetchone(): 結果セットから1行分のデータをタプルとして取得します。データがない場合はNoneを返します。
  • fetchall(): 結果セットの全ての行をタプルのリストとして取得します。データがない場合は空のリストを返します。
  • fetchmany(size): 結果セットから指定したsize行分のデータをタプルのリストとして取得します。

# 全てのユーザーデータを取得
sql_select_all = "SELECT id, name, email, age FROM users;"
cursor.execute(sql_select_all)
all_users = cursor.fetchall()
print("\n--- 全ユーザーデータ ---")
if all_users:
    for user in all_users:
        print(user) # (1, 'Alice Wonderland', 'alice@example.com', 30), ...
else:
    print("データが見つかりません。")

# 特定の条件でユーザーデータを取得 (例: 40歳以上のユーザー)
sql_select_age = "SELECT name, age FROM users WHERE age >= ?;"
cursor.execute(sql_select_age, (40,))
older_users = cursor.fetchall()
print("\n--- 40歳以上のユーザー ---")
if older_users:
    for user in older_users:
        print(f"名前: {user[0]}, 年齢: {user[1]}")
else:
    print("該当するユーザーが見つかりません。")

# 1件だけ取得
sql_select_one = "SELECT name FROM users WHERE id = ?;"
cursor.execute(sql_select_one, (1,))
one_user = cursor.fetchone()
print("\n--- ID=1のユーザー ---")
if one_user:
    print(f"名前: {one_user[0]}") # ('Alice Wonderland',) のタプルの0番目
else:
    print("ID=1のユーザーが見つかりません。")

      

デフォルトでは、取得されるデータはタプル形式です。カラム名でアクセスしたい場合は、後述するsqlite3.Rowを使用します。

既存のデータを更新するには、SQLのUPDATE文を使用します。ここでもプレースホルダを使うことが重要です。


# Aliceの年齢を更新
sql_update = "UPDATE users SET age = ? WHERE name = ?;"
new_age = 31
target_name = 'Alice Wonderland'

try:
    cursor.execute(sql_update, (new_age, target_name))
    print(f"\n{target_name} の年齢を {new_age} に更新しました。")
    conn.commit() # 変更を確定
    print("変更をコミットしました。✅")
except sqlite3.Error as e:
    print(f"データの更新に失敗しました: {e}")
    conn.rollback()

      

データを削除するには、SQLのDELETE文を使用します。WHERE句で削除対象の行を指定します。WHERE句を省略すると、テーブル内の全てのデータが削除されるので注意が必要です。😱


# Charlie Chaplinのデータを削除
sql_delete = "DELETE FROM users WHERE name = ?;"
delete_name = 'Charlie Chaplin'

try:
    cursor.execute(sql_delete, (delete_name,))
    # 影響を受けた行数を取得
    rows_affected = cursor.rowcount
    if rows_affected > 0:
        print(f"\n{delete_name} のデータを削除しました。({rows_affected}行)")
        conn.commit() # 変更を確定
        print("変更をコミットしました。✅")
    else:
        print(f"{delete_name} のデータは見つかりませんでした。")
except sqlite3.Error as e:
    print(f"データの削除に失敗しました: {e}")
    conn.rollback()

      

INSERT, UPDATE, DELETE文を実行した後、cursor.rowcount属性で影響を受けた行数を取得できます。SELECT文の場合は-1が返ることが多いです。

データベースの操作が完了したら、必ず接続を閉じる必要があります。これにより、リソースが解放され、ファイルへの変更が確実に書き込まれます。


# 接続を閉じる
if conn:
    conn.close()
    print("\nデータベース接続を閉じました。👋")
      

後述するwith構文を使うと、接続のクローズを自動的に行うことができるため、より安全で推奨される方法です。

トランザクション管理

トランザクションは、一連のデータベース操作を一つのまとまった処理単位として扱う仕組みです。銀行の振込処理のように、「口座Aから引き落とす」と「口座Bに入金する」が一連の処理として成功するか、あるいは両方とも失敗する(元の状態に戻る)かのどちらかでなければなりません。このような処理の原子性(Atomicity)、一貫性(Consistency)、独立性(Isolation)、永続性(Durability)を保証するのがトランザクションの役割で、これらを合わせてACID特性と呼びます。🏦

Pythonのsqlite3モジュールでは、デフォルトでトランザクションが自動的に管理されます。connect()時にisolation_levelパラメータを指定することで、トランザクションの挙動を制御できます。

  • None (Autocommit Mode): 各SQL文が実行されるたびに自動的にコミットされます。明示的なcommit()rollback()は不要ですが、一連の操作をまとめたい場合には不向きです。
  • "DEFERRED" (デフォルト): トランザクションは最初のデータ変更操作(INSERT, UPDATE, DELETEなど)が行われるまで開始されません。commit()が呼び出されるまでロックは最小限に抑えられます。
  • "IMMEDIATE": トランザクションは即座に開始され、データベースに対する書き込みロック(RESERVEDロック)を取得します。他の接続からの書き込みはブロックされますが、読み取りは可能です。
  • "EXCLUSIVE": トランザクションは即座に開始され、データベースに対する排他ロック(EXCLUSIVEロック)を取得します。他の接続からの書き込みも読み取りもブロックされます。

通常はデフォルトの"DEFERRED"で問題ありませんが、複数のプロセスやスレッドから同時にデータベースアクセスがある場合は、ロックの挙動を理解し、適切なisolation_levelを選択することが重要です。

明示的にトランザクションを制御するには、connection.commit()connection.rollback()を使用します。


import sqlite3

conn = None # finally節でエラーにならないように初期化
try:
    conn = sqlite3.connect('mydatabase.db') # デフォルトは DEFERRED
    cursor = conn.cursor()

    # --- トランザクション開始 (暗黙的) ---
    print("トランザクションを開始します...")

    # 操作1: 新しいユーザーを追加
    cursor.execute("INSERT INTO users (name, email, age) VALUES (?, ?, ?);",
                   ('David Copperfield', 'david@example.com', 60))
    print("ユーザー 'David Copperfield' を追加しようとしています...")

    # 操作2: Aliceのメールアドレスを更新 (存在しない場合エラーになる可能性)
    cursor.execute("UPDATE users SET email = ? WHERE name = ?;",
                   ('alice.wonder@example.com', 'Alice Wonderland'))
    print("ユーザー 'Alice Wonderland' のメールアドレスを更新しようとしています...")

    # 全ての操作が成功した場合のみコミット
    conn.commit()
    print("トランザクションが正常にコミットされました!✨")

except sqlite3.Error as e:
    # 何かエラーが発生したらロールバック
    print(f"エラーが発生したため、トランザクションをロールバックします: {e}")
    if conn:
        conn.rollback()
        print("ロールバックが完了しました。⏪")

finally:
    # 最後に必ず接続を閉じる
    if conn:
        conn.close()
        print("データベース接続を閉じました。")

      

この例では、commit()が呼び出される前にエラーが発生した場合、rollback()によってトランザクション開始前の状態に戻され、David Copperfieldの追加もAliceのメール更新も行われません。

Context Manager (`with`構文) の活用

Connectionオブジェクトはコンテキストマネージャとしても機能します。with構文を使用すると、ブロック内の処理が正常に終了した場合には自動的にcommit()が呼び出され、例外が発生した場合には自動的にrollback()が呼び出されます。さらに、withブロックを抜ける際に自動的にclose()が呼び出されるわけではありませんが、トランザクション管理が非常に簡潔かつ安全になります。これは推奨される書き方です。👍


import sqlite3

db_file = 'mydatabase.db'
new_user_data = ('Eve Online', 'eve@example.com', 25)
update_email_data = ('bob.the.builder@example.com', 'Bob The Builder')

try:
    # with構文で接続オブジェクトを取得
    with sqlite3.connect(db_file) as conn:
        cursor = conn.cursor()
        print("データベースに接続し、トランザクションを開始しました (with構文)。")

        # データの挿入
        cursor.execute("INSERT INTO users (name, email, age) VALUES (?, ?, ?);", new_user_data)
        print(f"ユーザー {new_user_data[0]} を追加しました。")

        # データの更新
        cursor.execute("UPDATE users SET email = ? WHERE name = ?;", update_email_data)
        print(f"ユーザー {update_email_data[1]} のメールアドレスを更新しました。")

        # このブロックを正常に抜けると自動的にコミットされる
        print("処理が正常に完了しました。自動的にコミットされます。")

except sqlite3.Error as e:
    # エラーが発生すると自動的にロールバックされる
    print(f"エラーが発生しました: {e}")
    print("自動的にロールバックされました。⏪")

finally:
    # with構文は接続を自動で閉じないため、必要であれば手動で閉じる
    # しかし、多くの場合、スクリプト終了時に自動的に閉じられる
    # if conn:
    #     conn.close()
    #     print("接続を閉じました。")
    print("withブロックを抜けました。")

      

with構文を使うことで、commit()rollback()の呼び出し忘れを防ぎ、コードの可読性も向上します。

データ型と `sqlite3.Row`

SQLite自体がネイティブにサポートするデータ型は少ないですが(TEXT, INTEGER, REAL, BLOB, NULL)、非常に柔軟な型システム(ダイナミックタイピング、またはマニフェストタイピングとも呼ばれる)を持っています。テーブル定義時に指定した型(例: VARCHAR, BOOLEAN, DATE)は「型アフィニティ」として解釈され、実際に格納される値の型に応じて内部的な型(TEXT, INTEGERなど)が決まります。

SQLite型アフィニティ 含まれるキーワード例 Pythonでの主な対応型 説明
TEXT CHAR, CLOB, TEXT str 文字列データを格納します。
NUMERIC NUMERIC, DECIMAL, BOOLEAN, DATE, DATETIME int, float, str など 数値またはテキストとして格納されます。値を挿入する際に、可能であればINTEGERやREALへの変換が試みられます。
INTEGER INT, INTEGER, TINYINT, BIGINT int 整数値を格納します。
REAL REAL, FLOAT, DOUBLE float 浮動小数点数を格納します。
BLOB (または型指定なし) BLOB bytes バイナリデータ(画像、ファイルなど)をそのまま格納します。
NULL N/A None NULL値を表します。

Pythonのsqlite3モジュールは、Pythonの型とSQLiteの型の間で自動的に変換(適合と変換)を行ってくれます。

  • Python → SQLite (適合): PythonのstrTEXTintINTEGERfloatREALbytesBLOBNoneNULLに変換されます。
  • SQLite → Python (変換): デフォルトでは、SQLiteの型に関わらず、データはPythonの対応する基本的な型(str, int, float, bytes, None)として返されます。connect()時にdetect_types=sqlite3.PARSE_DECLTYPESdetect_types=sqlite3.PARSE_COLNAMESを指定することで、より高度な型変換(例: DATE型カラムからPythonのdatetime.dateオブジェクトへ)が可能になりますが、別途コンバーター関数の登録が必要です。

デフォルトでは、SELECT文の結果はタプルで返されるため、カラムの値にはインデックス(row[0], row[1]など)でアクセスする必要があります。これではコードが読みにくくなることがあります。

Connectionオブジェクトのrow_factory属性にsqlite3.Rowを設定すると、結果がsqlite3.Rowオブジェクトのリストとして返されるようになります。Rowオブジェクトは、インデックスアクセスとカラム名(辞書ライクなキーアクセス)の両方をサポートします。✨


import sqlite3

db_file = 'mydatabase.db'

try:
    conn = sqlite3.connect(db_file)
    # Rowファクトリを設定
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()
    print("データベースに接続し、Rowファクトリを設定しました。")

    sql_select = "SELECT id, name, email FROM users WHERE age < ?;"
    cursor.execute(sql_select, (50,))
    young_users = cursor.fetchall()

    print("\n--- 50歳未満のユーザー (Rowオブジェクト) ---")
    if young_users:
        for user in young_users:
            print(f"ID: {user['id']}, 名前: {user['name']}, メール: {user['email']}")
            # インデックスアクセスも可能
            # print(f"ID: {user[0]}, 名前: {user[1]}, メール: {user[2]}")
    else:
        print("該当するユーザーが見つかりません。")

except sqlite3.Error as e:
    print(f"エラーが発生しました: {e}")
finally:
    if conn:
        conn.close()
        print("\nデータベース接続を閉じました。")

      

conn.row_factory = sqlite3.Rowを設定することで、user['name']のようにカラム名でデータにアクセスできるようになり、コードの可読性が大幅に向上します。

ベストプラクティスと注意点

sqlite3モジュールを効果的かつ安全に使用するためのベストプラクティスと注意点をいくつか紹介します。

  • SQLインジェクション対策: ユーザー入力などの外部データをSQL文に含める場合は、必ずプレースホルダ (?) を使用してください。文字列結合やフォーマット文字列(f-stringなど)でSQL文を組み立てるのは非常に危険です。🚨
    
    # 悪い例 (SQLインジェクションの危険性あり!) 😵
    user_input = "Alice'; DROP TABLE users; --"
    # cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'") # 絶対にダメ!
    
    # 良い例 (プレースホルダを使用) 😊
    cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))
                
  • with構文の使用: トランザクション管理(commit/rollback)を自動化し、コードを簡潔にするために、Connectionオブジェクトに対してwith構文を使用することを強く推奨します。
  • 接続とカーソルのクローズ: with構文を使わない場合は、try...finallyブロックを使用して、処理が終了したら必ずcursor.close()connection.close()を呼び出すようにしてください。(ただし、Connectionを閉じれば、そのConnectionから作成されたCursorも通常は無効になります)。リソースリークを防ぐために重要です。
  • トランザクションの適切な利用: 複数の変更操作を伴う場合は、それらを一つのトランザクションにまとめることで、データの一貫性を保ち、パフォーマンスを向上させることができます。頻繁なコミットはディスクI/Oを増加させ、遅くなる可能性があります。
  • エラーハンドリング: データベース操作は様々な理由で失敗する可能性があります(制約違反、ディスクエラー、ロック競合など)。try...except sqlite3.Errorブロックを使用して、適切なエラーハンドリングと、必要に応じたロールバックを行ってください。
  • データ型の考慮: SQLiteの型システムは柔軟ですが、意図しない型変換が発生することもあります。特にNUMERICアフィニティの挙動には注意が必要です。可能であれば、テーブル定義で適切な型アフィニティを指定しましょう。
  • パフォーマンス:
    • 頻繁に検索条件となるカラムにはインデックスを作成(CREATE INDEX文)することで、SELECTクエリの速度が大幅に向上することがあります。
    • 一度に大量のデータを挿入する場合は、executemany()を使用し、ループ内で個別にexecute()commit()を繰り返すよりも効率的です。
    • WAL (Write-Ahead Logging) モードを有効にすると(PRAGMA journal_mode=WAL;)、読み取りと書き込みの並行性が向上することがあります。
  • データベースファイルのパス: 相対パスでデータベースファイルを指定すると、スクリプトの実行場所によって挙動が変わる可能性があります。必要に応じて絶対パスを使用するか、osモジュールなどでパスを適切に管理してください。

SQLite/sqlite3のユースケース

SQLiteとsqlite3モジュールは、その手軽さと軽快さから、様々な用途に適しています。

🚀 アプリケーション設定の保存

簡単なキーバリューストアや、もう少し複雑な設定情報を保存するのに便利です。

🧪 プロトタイピングとテスト

本格的なデータベースサーバーをセットアップする前に、アプリケーションのデータ層を素早く開発・テストできます。

💻 デスクトップアプリケーション

ユーザー固有のデータやドキュメントデータをローカルに保存するのに適しています。

🌐 小規模Webサイト

アクセス数がそれほど多くない個人ブログや小規模な社内ツールなどのバックエンドとして利用できます。

📊 データ分析の中間ストレージ

CSVファイルなどのデータを一旦SQLiteに取り込み、SQLで前処理や集計を行うのに便利です。

一方で、大量の同時書き込みアクセスが発生するような高負荷なWebアプリケーションや、非常に大規模なデータセットを扱う場合には、PostgreSQLやMySQLのような本格的なクライアント/サーバー型データベースの利用を検討すべきです。

まとめ

Pythonの標準ライブラリsqlite3は、軽量データベースSQLiteを簡単かつ効果的に操作するための強力なツールです。基本的なCRUD操作からトランザクション管理、データ型の扱い、そして安全なコーディングのためのベストプラクティスまで、この記事で解説した内容を理解すれば、様々な場面でSQLiteを活用できるようになるでしょう。

特に、プレースホルダの使用with構文の活用は、安全で堅牢なコードを書く上で非常に重要です。ぜひこれらのテクニックを身につけて、PythonとSQLiteを使ったデータ管理を楽しんでください! 🎉🐍💾

コメント

タイトルとURLをコピーしました