このページは、生のバイト列バッファを直接扱わずに、C++ 構造体を AkkaraDB Native に保存したい場合の入口です。
include と open
Section titled “include と open”高レベル API は akkaradb/AkkaraDB.hpp から使います。
#include "akkaradb/AkkaraDB.hpp"
#include <cstdint>#include <string>
struct User { uint64_t id; std::string name; uint32_t age;};
AKKARADB_ENTITY(User, id, name, age);
int main() { auto db = akkaradb::AkkaraDB::open("data/app", akkaradb::StartupMode::NORMAL); auto users = db->table<&User::id>("users");
users.put(User{1, "Alice", 30}); auto alice = users.get(1);}一番単純な入口は AkkaraDB::open(path, mode) です。エンジンのしきい値、VersionLog、コーデック、Blob、API サーバー、起動プロファイルなどを細かく指定したい場合は AkkaraDB::open(AkkaraDB::Options) を使います。
StartupMode
Section titled “StartupMode”StartupMode は下層のエンジン設定へ渡される起動モードです。
| Mode | 主な用途 |
|---|---|
ULTRA_FAST | smoke test、benchmark、短時間のローカル実験。WAL、SST、Blob、Manifest、VersionLog を無効化し、close 時の強制 flush / sync も無効にして、大きめの MemTable しきい値を使います。 |
FAST | 起動速度を優先したい場合。WAL は非同期 sync、VersionLog は無効、SST read promotion は有効、大きめの MemTable しきい値を使います。 |
NORMAL | 一般的な組み込み利用。WAL は非同期 sync で、コンポーネントは通常の default set を使います。 |
DURABLE | 起動速度や書き込み速度より耐久性を優先したい場合。WAL は sync、VersionLog は有効です。 |
正確な低レベルオプションへの展開はエンジン層の責務です。明示的に制御したい場合は AkkaraDB::Options::overrides を使います。
AkkaraDB::Options::overrides では、シャードごとの MemTable しきい値、VersionLog、SST コーデック、Blob コーデック、Blob しきい値、SST 読み取り昇格、Bloom filter のキーあたりビット数、L0 SST ファイル数の上限を上書きできます。Options::api は API サーバーのバックエンド、ポート、通信方式、TLS / PSK、HTTP、TCP、gRPC の制限値を下層のエンジンオプションに渡します。
エンティティを定義する
Section titled “エンティティを定義する”PackedTable は、主キーフィールドへのメンバポインタで型付けされます。
struct Profile { uint64_t id; std::string email; std::string name; uint32_t age;};
AKKARADB_ENTITY(Profile, id, email, name, age);
auto profiles = db->table<&Profile::id>("profiles");AKKARADB_ENTITY(Type, PrimaryKey, ...) は次の情報を生成します。
| 生成される情報 | 用途 |
|---|---|
RefTraits<Type> | Ref<Type>、スキーマ登録、行 ID 参照が主キーを知るために使います。 |
| クエリ用フィールド | table.query([](auto row) { ... }) の中でフィールドを参照するために使います。 |
Ref<T> 用の trait が不要で、クエリ用フィールドだけを登録したい場合は AKKARADB_QUERYABLE(Type, ...) を使います。
BinPack の対応形式
Section titled “BinPack の対応形式”エンティティは BinPack でエンコードされます。集約型構造体は Boost.PFR で宣言順にフィールドをエンコードするため、ストレージ互換性はフィールド順と型の配置に依存します。
| 形 | 補足 |
|---|---|
bool、整数、float、double | 組み込みアダプタがあります。 |
enum | underlying integer type でエンコードされます。 |
std::string、std::string_view | string_view アダプタは書き込み専用です。保存値を読む用途では所有権を持つ文字列を使います。 |
std::vector<T>、std::vector<uint8_t>、std::array<T, N> | 要素にもアダプタが必要です。 |
std::map<K, V>、std::unordered_map<K, V> | キーと値にもアダプタが必要です。 |
std::optional<T> | 存在フラグと、値がある場合の値をエンコードします。 |
std::pair<A, B>、std::tuple<Ts...> | 要素ごとにエンコードします。 |
| 集約型構造体 | trivially copyable な集約型は memcpy の高速経路、それ以外はフィールドごとに処理します。 |
akkaradb::Ref<T> | 参照先の行 ID をエンコードします。 |
akkaradb::Immutable<T> | 包んでいる値をエンコードし、デコード時は seal 済みの値として戻ります。 |
データが存在した後のフィールド順、フィールド型、テーブル名の変更は、ストレージ移行として扱います。
PackedTable は主キーごとに 1 つのエンコード済みエンティティを保存します。
bool found = profiles.exists(1);auto profile = profiles.get(1);
Profile out{};bool decoded = profiles.getInto(1, out);
profiles.remove(1);| メソッド | 戻り値 | 内容 |
|---|---|---|
put(entity) | void | エンティティの主キーで挿入または置換します。 |
get(pk) | std::optional<Entity> | 見つかった場合にデコードしたエンティティを返します。 |
getInto(pk, out) | bool | 既存オブジェクトにデコードし、行が存在したかを返します。 |
exists(pk) | bool | テーブル行キーの存在を確認します。 |
remove(pk) | void | 行、テーブルインデックス、行 ID メタデータを削除します。 |
upsert(pk, fn) | void | エンティティを読み込むかデフォルト構築し、主キーを代入してから fn を実行して保存します。 |
updatePrimaryKey(oldPk, entity) | void | 安定した行 ID を保ったまま、行を entity の主キーへ移動します。 |
rowIdOf(pk) | std::optional<RowId> | 主キーから内部の行 ID を引きます。 |
primaryKeyOf(rowId) | std::optional<PK> | 行 ID から現在の主キーを引きます。 |
getByRowId(rowId) | std::optional<Entity> | 行 ID メタデータ経由で現在のエンティティを読みます。 |
getIntoByRowId(rowId, out) | bool | 行 ID 版の getInto です。 |
count() | size_t | テーブルのキー範囲内にある行数を数えます。 |
upsert
Section titled “upsert”現在値を見て更新したい場合は upsert() が使いやすいです。
profiles.upsert(1, [](Profile& profile) { profile.id = 1; profile.name = "Alice Updated"; profile.age += 1;});テーブルはコールバックの前に entity.*PrimaryKeyPtr = pk を代入します。コールバックではその主キーを維持するのが基本です。主キー自体を変える場合は updatePrimaryKey() を使います。
主キーを変更する
Section titled “主キーを変更する”updatePrimaryKey() は既存行を新しい主キーに移動し、内部の行 ID は維持します。
profiles.updatePrimaryKey( 1,);Ref<T> は見えている主キーのバイト列ではなく、行 ID メタデータで解決されるため、これは重要です。移動先の主キーが既に存在する場合は失敗します。
scanAll() と scan() は、hasNext() / next() を持つカーソル風の範囲を返します。
auto rows = profiles.scanAll();while (rows.hasNext()) { auto entry = rows.next(); // entry.id が主キー、entry.value がデコード済み Profile。}範囲スキャンは主キー順で進みます。
auto page = profiles.scan(100ULL, 200ULL);scan(startPk) は startPk からテーブル名前空間の終端までスキャンします。数値の主キーはソート可能にエンコードされるため、signed / unsigned の数値キーは自然な数値順でスキャンできます。
インデックス
Section titled “インデックス”インデックスはメンバポインタで登録します。
auto byAge = profiles.index<&Profile::age>();auto adults = byAge.range(18, 120);戻り値を保持しない場合は、indexed() で連結できます。
profiles.indexed<&Profile::email>() .indexed<&Profile::age>() .indexed<&Profile::name>();文字列 prefix search 用には、通常のフィールドインデックスとは別に prefixIndexed<&Field>() を登録します。string-like field 専用で、findBy<&Field>() には使われません。頻繁な startsWith() や、単純な末尾 % の like("prefix%") に使います。
profiles.prefixIndexed<&Profile::email>();findBy<&Field>(value) は最初に見つかったエンティティを返します。このフィールドのインデックスが登録されている必要があります。
インデックスは put()、remove()、updatePrimaryKey() のときに更新されます。既存行に対する自動反映はないため、インデックスはそのインデックスで探したい行を書く前に登録します。
C++ のクエリ API は、演算子オーバーロードとプロキシオブジェクトで型付きの式をコンパイル時に組み立てます。この層では、実行時ラムダ解釈や追加のソース書き換えは使いません。
auto result = profiles .query([](auto profile) { }) .toVector();クエリプロキシは AKKARADB_ENTITY または AKKARADB_QUERYABLE で生成されます。対応している演算子は、等価、不等価、比較、論理 && / ||、in、notIn、startsWith、contains、like、null 確認、ネストしたフィールド、マップからの値取得です。
| メソッド | 内容 |
|---|---|
where(fn) | 述語を論理 AND で追加します。 |
first() | std::optional<Entry> を返します。 |
any() | 1 件でも一致するかを返します。 |
count() | 一致件数を数えます。 |
toVector() | 一致した項目を std::vector に展開します。 |
クエリ結果の順序は安定した API 契約ではありません。順序が必要な場合は、実体化した結果をアプリケーション側で sort します。
インデックスを使う計画
Section titled “インデックスを使う計画”計画器は、登録済みインデックスを使える述語を探します。返す前には常に完全な式で絞り込むため、インデックスは候補集合を狭める役割です。
| 述語の形 | インデックス計画 |
|---|---|
field == literal | フィールドインデックスの等価範囲。 |
field != literal | フィールドインデックス全体をスキャンし、式で絞り込みます。 |
数値の field >/>=/</<= literal | bool 以外の算術フィールドでは順序付きインデックス範囲。 |
field.in(values) | 値ごとの等価範囲。候補の主キーは重複排除されます。 |
field.notIn(values) | フィールドインデックス全体をスキャンし、式で絞り込みます。 |
field.isNull() | 空の optional に対応するエンコード済み値の等価範囲。 |
field.isNotNull() | フィールドインデックス全体をスキャンし、式で絞り込みます。 |
startsWith | prefixIndexed<&Field>() があれば prefix index の範囲。なければ通常のフィールドインデックス全体を scan し、式で絞り込みます。 |
contains | フィールドインデックス全体を scan し、式で絞り込みます。 |
like("exact") | 等価範囲。 |
like("prefix%") | 単純な prefix pattern で prefixIndexed<&Field>() があれば prefix index の範囲。なければ通常のフィールドインデックス全体を scan し、式で絞り込みます。 |
&& の中の述語 | 計画器が使えるインデックス側を score で選びます。 |
| ` | |
| ネストしたフィールド、map からの値取得 | 正しく評価されますが、それ単体では現在 index 起点にはなりません。 |
プランナーの優先順位、リテラル変換、like() のワイルドカード、クエリラムダ式の境界は Query を参照します。
optional / nested / map フィールド
Section titled “optional / nested / map フィールド”クエリプロキシは optional field、nested struct field、map lookup を扱えます。
auto unnamed = users.query([](auto user) { return user.nickname.isNull();});
auto tokyo = users.query([](auto user) { return user.address.template field<&Address::city>() == "Tokyo";});
auto gold = users.query([](auto user) { return user.tags.get("tier") == std::optional<std::string>{"gold"};});ネストしたフィールドは依存名のテンプレート呼び出しになるため、template field<&Nested::field>() と書きます。
変更不可フィールド
Section titled “変更不可フィールド”作成時には設定できるが、保存後は変更したくないフィールドには akkaradb::Immutable<T> を使います。
struct Account { uint64_t id; akkaradb::Immutable<std::string> handle;};put() は読み込み後または書き込み後に immutable field を seal します。seal 済みのフィールドを後から変更しようとすると例外になります。主キーフィールドには Immutable<T> を使えません。
onUpdate<&Field>() は、既存行の置き換えで対象フィールドが変わったときだけ実行されます。
profiles.onUpdate<&Profile::email>([](const Profile& oldValue, Profile& newValue) { newValue.email = normalizeEmail(newValue.email);});ハンドラは古いエンティティを見ながら、エンコード前の新しいエンティティを変更できます。正規化、派生フィールド、小さな検証に向いています。テーブルの書き込み経路の中で実行されるため、重い副作用はフックの外に置く方が扱いやすいです。
Ref<T> は、主キー、行 ID、読み込み済み値のいずれかを持ち、テーブル接続情報が取り付けられると遅延解決できます。
struct Author { uint64_t id; std::string name;};
struct Post { uint64_t id; akkaradb::Ref<Author> author; std::string title;};
AKKARADB_ENTITY(Author, id, name);AKKARADB_ENTITY(Post, id, author, title);
authors.put({1, "Alice"});posts.put({100, akkaradb::ref<Author>(1), "Hello"});
auto post = posts.get(100);auto name = post->author->name;Ref<T> をエンティティから作った場合、その参照先エンティティは dirty と見なされます。PackedTable::put() は owner エンティティを書く前に、dirty な参照先を保存します。
結合は、スキャンと検索の上にある型付きビューです。
auto joined = posts .join<&Post::author>(authors) .where([](const Post& post, const Author& author) { return author.name == "Alice"; }) .toVector();通常フィールドで結合する場合は、左右のメンバポインタを指定します。
auto joined = posts.join<&Post::authorId, &Author::id>(authors).toVector();右側フィールドが右テーブルの主キーなら主キー検索を使います。それ以外の場合は、左側の行ごとに右テーブルをスキャンします。
スキーマと外部キー
Section titled “スキーマと外部キー”AkkaraDB::Schema は型付きテーブルを登録し、hook ベースの外部キー動作を設定します。
auto schema = db->schema() .table<&Author::id>("authors") .table<&Post::id>("posts") .foreignKey<&Post::author>({akkaradb::OnDelete::Cascade}, {akkaradb::OnUpdate::Cascade}) .open();
auto& authors = schema.table<Author>();auto& posts = schema.table<Post>();通常フィールドの関連では、参照元フィールドと参照先フィールドを指定します。
schema.foreignKey<&PlainPost::authorId, &Author::id>( {akkaradb::OnDelete::Restrict}, {akkaradb::OnUpdate::Cascade});外部キー動作
Section titled “外部キー動作”| 動作 | 削除時 | 主キー更新時 |
|---|---|---|
Restrict | 参照されている参照先の削除を拒否します。 | 参照されている参照先キーの移動を拒否します。 |
Cascade | 参照先を参照する参照元行を削除します。 | 参照元フィールドを新しい参照先キーに書き換えます。 |
SetNull | null 許容の参照元フィールドを std::nullopt にします。 | null 許容の参照元フィールドを std::nullopt にします。 |
削除用に 1 つ、更新用に 1 つだけ動作を選べます。SetNull には null 許容の参照元フィールドが必要です。外部キーは専用の制約サブシステムではなく、テーブルフックとスキャンで実装されています。
参照先フィールドが参照先テーブルの主キーではない場合、foreignKey<FieldPtr, TargetFieldPtr>() は参照先フィールドのインデックスを登録し、存在確認にインデックス検索を使います。削除 / 更新時の動作では、それでも参照元行を見つけるために参照元テーブルをスキャンします。Ref<T> フィールドでは削除時の動作は参照先主キー前提で、更新時の動作は行 ID が安定しているため実質的に省略されます。
低レベル API との関係
Section titled “低レベル API との関係”Native 高レベル API の型付きデータは、低レベル API と同じエンジンのキー空間に保存されます。下層エンジンで VersionLog が有効なら、型付き行も最終的にはエンジンのキー / 値書き込みなので、同じバージョン履歴の対象になります。
この C++ 高レベルヘッダが現在公開しているのは、現在行の CRUD、行 ID 検索、走査、クエリ、インデックス、参照、結合、スキーマ動作です。PackedTable には型付きの getAt() や history() ヘルパーは見えていないため、明示的な過去時点読み取りやロールバック制御が必要な場合は db->engine() から低レベルの VersionLog API を使います。
よくある失敗
Section titled “よくある失敗”| 症状 | 確認すること |
|---|---|
findBy<&Field>() が例外になる | index<&Field>() または indexed<&Field>() でフィールドインデックスを登録していません。 |
| 新しく登録したインデックスが古い行を見つけない | インデックス登録は既存行を自動反映しません。登録後に行を書き直します。 |
Ref<T> が解決できない | 参照が未接続、参照先テーブルがスキーマ / 接続情報に登録されていない、または参照先行が削除されています。 |
foreignKey<&RefField>() が参照先テーブル未登録で失敗する | スキーマに参照先エンティティのテーブルが含まれていません。 |
SetNull の設定が失敗する | 参照元フィールドが std::optional<T> ではありません。 |
updatePrimaryKey() が移動先ありで失敗する | 新しい主キーが既に存在します。 |
読み込んだ Immutable<T> フィールドの代入が失敗する | デコードまたは保存後に値が seal されています。 |
| テーブル名変更後にデータが見えない | テーブル名はハッシュ化された保存接頭辞の一部です。 |
| 構造体変更後にデコード前提が崩れる | BinPack の集約型エンコードはフィールド順と互換なフィールド型に依存します。 |
実装時の目安
Section titled “実装時の目安”- 安定した主キーフィールドを持つ通常の構造体を定義する。
AKKARADB_ENTITYで保存対象フィールドを列挙する。AkkaraDB::open()でデータベースを開く。- テーブルを開いた直後にインデックスを登録する。
- 型付き絞り込みには
query()、インデックス付き等価検索にはfindBy()を使う。 - 参照や外部キーが必要なら
Schemaを使ってテーブル間の関係をまとめる。