简体中文 English 繁體中文 日本語 Русский
本文为静态镜像,内容以交互版为准 在交互式文档中心打开 →

SQL クエリビルダー

ErisPulse の Storage モジュールは、チェーン呼び出しスタイルの汎用 SQL クエリビルダーを提供し、カスタムテーブルの作成、クエリ、更新、削除操作をサポートします。

架構設計

Bases/storage.py                    Core/storage.py
┌─────────────────────┐             ┌──────────────────────────┐
│  BaseStorage (ABC)  │◄────────────│  StorageManager          │
│  BaseQueryBuilder   │             │  (SQLite concrete impl)  │
│    (ABC)            │             │                          │
└─────────────────────┘             │  SQLiteQueryBuilder      │
                                    │  AlterTableBuilder       │
                                    └──────────────────────────┘

導入

from ErisPulse import sdk
# または
from ErisPulse.Core import storage

# ABC 基底クラス(型注釈またはカスタム実装用)
from ErisPulse.Core.Bases.storage import BaseStorage, BaseQueryBuilder

テーブル管理

テーブル作成

sdk.storage.CreateTable("users", {
    "id": "INTEGER PRIMARY KEY AUTOINCREMENT",
    "name": "TEXT NOT NULL",
    "age": "INTEGER DEFAULT 0",
    "email": "TEXT"
})

テーブルの存在確認

if sdk.storage.HasTable("users"):
    print("users テーブルは既に存在しています")

テーブル削除

sdk.storage.DropTable("users")

テーブル構造の変更

# 列の追加
sdk.storage.AlterTable("users").AddColumn("email", "TEXT").Execute()

# テーブル名の変更
sdk.storage.AlterTable("users").RenameTo("members").Execute()

# 連鎖的な複数操作
sdk.storage.AlterTable("users") \
    .AddColumn("phone", "TEXT") \
    .AddColumn("address", "TEXT") \
    .Execute()

チェーン呼び出しによるクエリ

データの挿入

# 単一行挿入(辞書を渡す)
sdk.storage.Table("users").Insert({"name": "Alice", "age": 30}).Execute()

# バッチ挿入(辞書のリストを渡す)
sdk.storage.Table("users").InsertMulti([
    {"name": "Bob", "age": 25},
    {"name": "Charlie", "age": 35},
    {"name": "Dave", "age": 40}
]).Execute()

データの取得

重要:Select() は list[tuple](タプルのリスト)を返します。辞書ではなく、列の順序に従ってインデックスでアクセスする必要があります。

# 全列を取得
rows = sdk.storage.Table("users").Select().Execute()
# rows: [(1, "Alice", 30), (2, "Bob", 25), ...]

# 指定列を取得
rows = sdk.storage.Table("users").Select("name", "age").Execute()
# rows: [("Alice", 30), ("Bob", 25), ...]

# インデックスで値を取得
for row in rows:
    name = row[0]   # "Alice"
    age = row[1]    # 30

タプルを辞書に変換

ToDict() をチェーン内で呼び出すことを推奨します。SELECT の結果は自動的に辞書で返されます(列名 → 値):

# ToDict チェーン:結果は list[dict] で、列名はクエリメタデータから自動取得(SELECT * に対しても同様)
rows = sdk.storage.Table("users").Select("name", "age").ToDict().Execute()
# rows: [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}, ...]

for row in rows:
    print(row["name"], row["age"])

# ExecuteOne でも同様に有効
row = sdk.storage.Table("users").Select("name", "age") \
    .Where("id = ?", 1) \
    .ToDict() \
    .ExecuteOne()
# row: {"name": "Alice", "age": 30} または None

ToDict() はチェーン呼び出しのマーカー(self を返す)です:ToDict() を呼び出さないチェーンは元の list[tuple] の振る舞いを保ち、完全に後方互換性があります。copy() はこのマーカーを保持します。

手動で zip を使用する方法(ToDict と同等、チェーンを変更できない場合に適しています):

columns = ["id", "name", "age"]
rows = sdk.storage.Table("users").Select(*columns).Execute()

# 方法1:ループ内で zip
for row in rows:
    record = dict(zip(columns, row))
    print(record["name"], record["age"])

# 方法2:一度に辞書のリストに変換
records = [dict(zip(columns, row)) for row in rows]

単一行の取得

row = sdk.storage.Table("users").Select("name", "age") \
    .Where("id = ?", 1) \
    .ExecuteOne()

# row は tuple または None
if row is not None:
    name = row[0]  # "Alice"
    age = row[1]   # 30

条件フィルタ

Where(condition, *params) は、複数のパラメータを渡すことで、複数の ? プレースホルダに対応します。

# 単一条件(1つのプレースホルダ、1つのパラメータ)
rows = sdk.storage.Table("users").Select("name") \
    .Where("age > ?", 18) \
    .Execute()

# 1つの Where で複数のプレースホルダを使用
rows = sdk.storage.Table("users").Select("name") \
    .Where("age > ? AND age < ?", 20, 40) \
    .Execute()

# Where を複数回呼び出す(AND で連結)
rows = sdk.storage.Table("users").Select("name") \
    .Where("age > ?", 20) \
    .Where("age < ?", 40) \
    .Execute()

ソート、ページング

# 昇順
rows = sdk.storage.Table("users").Select("name", "age") \
    .OrderBy("name") \
    .Execute()

# 降順
rows = sdk.storage.Table("users").Select("name") \
    .OrderBy("age", desc=True) \
    .Execute()

# ページング
rows = sdk.storage.Table("users").Select("name") \
    .OrderBy("id") \
    .Limit(10) \
    .Offset(20) \
    .Execute()

データの更新

# 条件付き更新
sdk.storage.Table("users") \
    .Update({"age": 31}) \
    .Where("name = ?", "Alice") \
    .Execute()

# 全件更新
sdk.storage.Table("users") \
    .Update({"status": "active"}) \
    .Execute()

データの削除

# 条件付き削除
sdk.storage.Table("users") \
    .Delete() \
    .Where("name = ?", "Bob") \
    .Execute()

# 全件削除
sdk.storage.Table("users").Delete().Execute()

カウントと存在確認

# カウント
count = sdk.storage.Table("users").Count()
count = sdk.storage.Table("users").Where("age > ?", 18).Count()

# 存在確認
exists = sdk.storage.Table("users").Where("name = ?", "Alice").Exists()

クエリ条件の再利用

copy() を使用してビルダーを深くコピーし、基本条件を再利用します:

base = sdk.storage.Table("users").Where("age > ?", 20)

# 同じ条件に基づいてクエリを実行
rows = base.copy().Select("name").OrderBy("name").Limit(5).Execute()

# 同じ条件に基づいてカウント
count = base.copy().Count()

# 同じ条件に基づいて存在確認
exists = base.copy().Where("name = ?", "Alice").Exists()

ビルダーのリセット

builder = sdk.storage.Table("users").Select("name").Where("age > ?", 18)
builder.clear()

# クエリを再構築
builder.Select("name", "age").Where("name = ?", "Alice")
rows = builder.Execute()

トランザクションでの使用

チェーン呼び出しはトランザクションを完全にサポートします:

# トランザクションをコミット
with sdk.storage.transaction():
    sdk.storage.Table("users").Insert({"name": "Eve", "age": 22}).Execute()
    sdk.storage.Table("users").Update({"age": 23}).Where("name = ?", "Eve").Execute()

# ロールバックの例
try:
    with sdk.storage.transaction():
        sdk.storage.Table("users").Delete().Where("name = ?", "Alice").Execute()
        raise Exception("force rollback")
except Exception:
    pass
# Alice のレコードは依然として存在します

非同期のネイティブ API

2.8.0 以降、ストレージ層は非同期をネイティブの主インターフェースとしています。すべての終端メソッドには、a が付いた非同期版が用意されており、非同期ハンドラ内ではイベントループのブロッキングを避けるために推奨されます:

# 非同期トランザクション
async with sdk.storage.atransaction():
    await sdk.storage.aset("key1", "value1")
    await sdk.storage.aset("key2", {"nested": True})

# 非同期チェーンクエリ
rows = await sdk.storage.Table("users").Select("name", "age").ToDict().aExecute()
row = await sdk.storage.Table("users").Select("*").Where("id = ?", 1).aExecuteOne()
total = await sdk.storage.Table("users").Where("age > ?", 18).aCount()
exists = await sdk.storage.Table("users").Where("name = ?", "Alice").aExists()

# 非同期 KV
await sdk.storage.aset("app.name", "MyApp")
value = await sdk.storage.aget("app.name")
keys = await sdk.storage.aget_all_keys()
同期(互換層) 非同期ネイティブ
get / set / delete aget / aset / adelete
get_all_keys / clear aget_all_keys / aclear
get_multi / set_multi / delete_multi aget_multi / aset_multi / adelete_multi
transaction() atransaction()
CreateTable / DropTable / HasTable aCreateTable / aDropTable / aHasTable
Execute / ExecuteOne / Count / Exists aExecute / aExecuteOne / aCount / aExists

戻り値の説明

操作 戻り値の型 説明
Select().Execute() list[tuple] タプルのリスト、列の順序で並べ替え
Select().ExecuteOne() tuple | None 単一行のタプルまたは None
Insert().Execute() int 受影響された行数
InsertMulti().Execute() int 挿入された行数
Update().Execute() int 受影響された行数
Delete().Execute() int 受影響された行数
Count() int 一致する行数
Exists() bool 存在するか

戻り値の処理例

# Select はタプルを返し、インデックスで値を取得します
rows = sdk.storage.Table("users").Select("name", "age").Execute()
first_name = rows[0][0]  # 最初の行、最初の列 name
first_age = rows[0][1]   # 最初の行、2番目の列 age

# 推奨:列名リスト + zip を使って辞書に変換、コードの可読性が向上
cols = ["name", "age"]
rows = sdk.storage.Table("users").Select(*cols).Execute()
for row in rows:
    d = dict(zip(cols, row))
    print(d["name"], d["age"])

# ExecuteOne は単一行のタプルまたは None を返します
row = sdk.storage.Table("users").Select("name").Where("id = ?", 1).ExecuteOne()
name = row[0] if row else None

# Insert/Update/Delete は影響された行数を返します
affected = sdk.storage.Table("users").Delete().Where("age < ?", 18).Execute()
print(f"削除されたレコード数: {affected}")

パラメータ化されたクエリ

すべての WHERE パラメータは ? プレースホルダを使用し、パラメータは Where() の追加引数として渡します(タプルやリストではありません):

# 正しい ✓ — 複数のパラメータを個別に渡す
sdk.storage.Table("users").Where("age > ? AND name = ?", 18, "Alice").Execute()

# 正しい ✓ — Where を複数回呼び出す
sdk.storage.Table("users").Where("age > ?", 18).Where("name = ?", "Alice").Execute()

# 間違っている ✗ — タプルを渡さないでください
sdk.storage.Table("users").Where("age > ? AND name = ?", (18, "Alice")).Execute()
# これはタプル全体を最初のプレースホルダの値として扱います

# 間違っている ✗ — SQL インジェクションのリスクがあります
sdk.storage.Table("users").Where(f"name = '{user_input}'").Execute()

Where パラメータの渡し方

# Where(condition: str, *params: Any)
# params は可変引数で、個別に渡します

# 単一パラメータ
.Where("name = ?", "Alice")

# 複数パラメータ
.Where("age > ? AND age < ?", 18, 60)

# LIKE クエリ
.Where("name LIKE ?", "A%")

# IN クエリ(プレースホルダを手動で構築する必要があります)
.Where("name IN (?, ?, ?)", "Alice", "Bob", "Charlie")

カスタムストレージバックエンド

2.8.0 以降、抽象層は非同期メソッドをネイティブ契約としています:BaseStorage を継承して非同期抽象メソッドを実装し、同期 get/set/Execute などは基底クラスが自動的にブリッジで提供します:

from ErisPulse.Core.Bases.storage import BaseStorage, BaseQueryBuilder

class MyQueryBuilder(BaseQueryBuilder):
    async def aExecute(self):
        # 実際の実行ロジックを実装
        ...

    async def aExecuteOne(self):
        ...

    async def aCount(self):
        ...

    async def aExists(self):
        ...


class MyStorage(BaseStorage):
    async def aget(self, key, default=None):
        ...

    async def aset(self, key, value):
        ...

    # 他の非同期抽象メソッドとトランザクション接続のフックを実装 ...
    def Table(self, table_name):
        return MyQueryBuilder(self, table_name)

Tip

トランザクション接続ルーティング(conn キーワード引数)を実装したくない場合は、クラス属性を _SUPPORTS_CONN_ROUTING = False(デフォルト)のままにしておけば、トランザクション機能は使用可能ですが、隔離性は制限されます。 純粋な SQL バックエンドの場合は、Core/Bases/sql_base.py の SQLStorageBase + SQLQueryBuilder を直接継承し、接続管理と方言の実行フーニャーを提供するだけで済みます。詳しくは ストレージバックエンドを参照してください。

関連ドキュメント