SQL クエリビルダー
ErisPulse の Storage モジュールは、チェーン呼び出しスタイルの汎用 SQL クエリビルダーを提供し、カスタムテーブルの作成、クエリ、更新、削除操作をサポートします。
架構設計
Bases/storage.py Core/storage.py
┌─────────────────────┐ ┌──────────────────────────┐
│ BaseStorage (ABC) │◄────────────│ StorageManager │
│ BaseQueryBuilder │ │ (SQLite concrete impl) │
│ (ABC) │ │ │
└─────────────────────┘ │ SQLiteQueryBuilder │
│ AlterTableBuilder │
└──────────────────────────┘
BaseStorage/BaseQueryBuilderは抽象基底クラスで、統一されたインターフェースを定義し、将来的に他のストレージメディア(Redis、MySQL など)への拡張を可能にします。StorageManagerは現在の SQLite 具体実装で、完全に後方互換性を保ちます。
導入
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 を直接継承し、接続管理と方言の実行フーニャーを提供するだけで済みます。詳しくは
ストレージバックエンドを参照してください。
関連ドキュメント
- ストレージバックエンド - sqlite / mysql / postgres バックエンドの選択と設定
- コアモジュール API - Storage モジュールの完全な API
- ストレージ基底クラス API - BaseStorage/BaseQueryBuilder 抽象インターフェース
- メッセージビルダー - MessageBuilder のチェーン呼び出しスタイルの参考