コンテンツにスキップ

高レベル API の使い方

このページは、生のバイト列バッファを直接扱わずに、C++ 構造体を AkkaraDB Native に保存したい場合の入口です。

高レベル 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 は下層のエンジン設定へ渡される起動モードです。

Mode主な用途
ULTRA_FASTsmoke 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 の制限値を下層のエンジンオプションに渡します。

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 でエンコードされます。集約型構造体は Boost.PFR で宣言順にフィールドをエンコードするため、ストレージ互換性はフィールド順と型の配置に依存します。

補足
bool、整数、floatdouble組み込みアダプタがあります。
enumunderlying integer type でエンコードされます。
std::stringstd::string_viewstring_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 つのエンコード済みエンティティを保存します。

profiles.put(Profile{1, "[email protected]", "Alice", 30});
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() が使いやすいです。

profiles.upsert(1, [](Profile& profile) {
profile.id = 1;
profile.name = "Alice Updated";
profile.age += 1;
});

テーブルはコールバックの前に entity.*PrimaryKeyPtr = pk を代入します。コールバックではその主キーを維持するのが基本です。主キー自体を変える場合は updatePrimaryKey() を使います。

updatePrimaryKey() は既存行を新しい主キーに移動し、内部の行 ID は維持します。

profiles.updatePrimaryKey(
1,
Profile{10, "[email protected]", "Alice Cooper", 31}
);

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 の数値キーは自然な数値順でスキャンできます。

インデックスはメンバポインタで登録します。

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) {
return profile.email == "[email protected]" && profile.age >= 18;
})
.toVector();

クエリプロキシは AKKARADB_ENTITY または AKKARADB_QUERYABLE で生成されます。対応している演算子は、等価、不等価、比較、論理 && / ||innotInstartsWithcontainslike、null 確認、ネストしたフィールド、マップからの値取得です。

メソッド内容
where(fn)述語を論理 AND で追加します。
first()std::optional<Entry> を返します。
any()1 件でも一致するかを返します。
count()一致件数を数えます。
toVector()一致した項目を std::vector に展開します。

クエリ結果の順序は安定した API 契約ではありません。順序が必要な場合は、実体化した結果をアプリケーション側で sort します。

計画器は、登録済みインデックスを使える述語を探します。返す前には常に完全な式で絞り込むため、インデックスは候補集合を狭める役割です。

述語の形インデックス計画
field == literalフィールドインデックスの等価範囲。
field != literalフィールドインデックス全体をスキャンし、式で絞り込みます。
数値の field >/>=/</<= literalbool 以外の算術フィールドでは順序付きインデックス範囲。
field.in(values)値ごとの等価範囲。候補の主キーは重複排除されます。
field.notIn(values)フィールドインデックス全体をスキャンし、式で絞り込みます。
field.isNull()空の optional に対応するエンコード済み値の等価範囲。
field.isNotNull()フィールドインデックス全体をスキャンし、式で絞り込みます。
startsWithprefixIndexed<&Field>() があれば prefix index の範囲。なければ通常のフィールドインデックス全体を scan し、式で絞り込みます。
containsフィールドインデックス全体を scan し、式で絞り込みます。
like("exact")等価範囲。
like("prefix%")単純な prefix pattern で prefixIndexed<&Field>() があれば prefix index の範囲。なければ通常のフィールドインデックス全体を scan し、式で絞り込みます。
&& の中の述語計画器が使えるインデックス側を score で選びます。
`
ネストしたフィールド、map からの値取得正しく評価されますが、それ単体では現在 index 起点にはなりません。

プランナーの優先順位、リテラル変換、like() のワイルドカード、クエリラムダ式の境界は Query を参照します。

クエリプロキシは 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>() と書きます。

作成時には設定できるが、保存後は変更したくないフィールドには 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();

右側フィールドが右テーブルの主キーなら主キー検索を使います。それ以外の場合は、左側の行ごとに右テーブルをスキャンします。

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}
);
動作削除時主キー更新時
Restrict参照されている参照先の削除を拒否します。参照されている参照先キーの移動を拒否します。
Cascade参照先を参照する参照元行を削除します。参照元フィールドを新しい参照先キーに書き換えます。
SetNullnull 許容の参照元フィールドを std::nullopt にします。null 許容の参照元フィールドを std::nullopt にします。

削除用に 1 つ、更新用に 1 つだけ動作を選べます。SetNull には null 許容の参照元フィールドが必要です。外部キーは専用の制約サブシステムではなく、テーブルフックとスキャンで実装されています。

参照先フィールドが参照先テーブルの主キーではない場合、foreignKey<FieldPtr, TargetFieldPtr>() は参照先フィールドのインデックスを登録し、存在確認にインデックス検索を使います。削除 / 更新時の動作では、それでも参照元行を見つけるために参照元テーブルをスキャンします。Ref<T> フィールドでは削除時の動作は参照先主キー前提で、更新時の動作は行 ID が安定しているため実質的に省略されます。

Native 高レベル API の型付きデータは、低レベル API と同じエンジンのキー空間に保存されます。下層エンジンで VersionLog が有効なら、型付き行も最終的にはエンジンのキー / 値書き込みなので、同じバージョン履歴の対象になります。

この C++ 高レベルヘッダが現在公開しているのは、現在行の CRUD、行 ID 検索、走査、クエリ、インデックス、参照、結合、スキーマ動作です。PackedTable には型付きの getAt()history() ヘルパーは見えていないため、明示的な過去時点読み取りやロールバック制御が必要な場合は db->engine() から低レベルの VersionLog API を使います。

症状確認すること
findBy<&Field>() が例外になるindex<&Field>() または indexed<&Field>() でフィールドインデックスを登録していません。
新しく登録したインデックスが古い行を見つけないインデックス登録は既存行を自動反映しません。登録後に行を書き直します。
Ref<T> が解決できない参照が未接続、参照先テーブルがスキーマ / 接続情報に登録されていない、または参照先行が削除されています。
foreignKey<&RefField>() が参照先テーブル未登録で失敗するスキーマに参照先エンティティのテーブルが含まれていません。
SetNull の設定が失敗する参照元フィールドが std::optional<T> ではありません。
updatePrimaryKey() が移動先ありで失敗する新しい主キーが既に存在します。
読み込んだ Immutable<T> フィールドの代入が失敗するデコードまたは保存後に値が seal されています。
テーブル名変更後にデータが見えないテーブル名はハッシュ化された保存接頭辞の一部です。
構造体変更後にデコード前提が崩れるBinPack の集約型エンコードはフィールド順と互換なフィールド型に依存します。
  1. 安定した主キーフィールドを持つ通常の構造体を定義する。
  2. AKKARADB_ENTITY で保存対象フィールドを列挙する。
  3. AkkaraDB::open() でデータベースを開く。
  4. テーブルを開いた直後にインデックスを登録する。
  5. 型付き絞り込みには query()、インデックス付き等価検索には findBy() を使う。
  6. 参照や外部キーが必要なら Schema を使ってテーブル間の関係をまとめる。