1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

インポートするだけで使えるインメモリDBを作りました(DeltaTraceDB)

1
Posted at

インポートするだけで使えるNoSQLインメモリDBを作りました。

しかも

  • 全文検索(ネスト構造や正規表現にも対応。シンプルなスキャンベースで小〜中規模データを柔軟に検索可能)
  • トランザクション対応 (アトミック実行+失敗時は自動ロールバック)
  • 操作履歴確認(保存したクエリがログ)
  • 任意時点への巻き戻し

ができます。

つまるところ

データ整合性と追跡可能性が求められる現場に強い DBです。

例:

  • AIがデータを書き換えるアプリ
  • 操作履歴を完全に残したい業務システム

Flutter / Python から使えます。


パッケージ

Dart

Python

ドキュメント


クエリをそのまま送れるDB(ローカルでもサーバーでも同じ動作)

最小構成(とりあえず動かす)

import 'package:delta_trace_db/delta_trace_db.dart';

void main() {
  final db = DeltaTraceDatabase();
  // データ追加
  db.executeQuery(
    // Mapそのままで扱える簡易版
    RawQueryBuilder.add(
      target: 'users',
      rawAddData: [
        {"name": "Taro"},
        {"name": "Jiro"},
      ],
    ).build(),
  );

  // 検索(最小)
  final result = db.executeQuery(
    QueryBuilder.search(
      target: 'users',
      queryNode: FieldContains("name", "Ta"),
    ).build(),
  );

  // "Ta" を含むユーザーだけ返る
  print(result.result); // → [{name: Taro}]
}

型安全に扱う場合(実務向け)

import 'package:delta_trace_db/delta_trace_db.dart';
import 'package:file_state_manager/file_state_manager.dart';

class User extends CloneableFile {
  final int id;
  final String name;
  final int age;
  final DateTime createdAt;

  User({
    required this.id,
    required this.name,
    required this.age,
    required this.createdAt,
  });

  static User fromDict(Map<String, dynamic> src) => User(
    id: src['id'],
    name: src['name'],
    age: src['age'],
    createdAt: DateTime.parse(src['createdAt']),
  );

  @override
  Map<String, dynamic> toDict() => {
    'id': id,
    'name': name,
    'age': age,
    'createdAt': createdAt.toUtc().toIso8601String(),
  };

  @override
  User clone() {
    return User.fromDict(toDict());
  }
}

void main() {
  final db = DeltaTraceDatabase();
  final now = DateTime.now();

  List<User> users = [
    User(
        id: -1,
        name: 'Taro',
        age: 30,
        createdAt: now),
    User(
        id: -1,
        name: 'Jiro',
        age: 25,
        createdAt: now),
  ];

  final query = QueryBuilder.add(
    target: 'users',
    addData: users,
    serialKey: "id", // idをシリアルとして一意な値を自動割当
    returnData: true, // シリアル割当後のオブジェクトを返す
  ).build();

  // このクエリ自体がJSONとしてそのままサーバーに送れるため、クライアントとサーバーで同じDBロジックを共有できます。
  // このクエリそのものが「Delta(差分)」なので、これを保存すれば完全な履歴になります。
  final r = db.executeQuery<User>(query);

  // デシリアライズ用の関数を渡して、結果から直接オブジェクトにコンバート
  List<User> results = r.convert(User.fromDict);

  for(User i in results){
    print(i.toDict());
  }

  // db自体のスナップショットも取れます
  final snapShot = db.toDict();
}

高度な検索(ソート・ページング)

import 'package:delta_trace_db/delta_trace_db.dart';
import 'package:file_state_manager/file_state_manager.dart';

class User extends CloneableFile {
  final int id;
  final String name;
  final int age;
  final DateTime createdAt;

  User({
    required this.id,
    required this.name,
    required this.age,
    required this.createdAt,
  });

  static User fromDict(Map<String, dynamic> src) => User(
        id: src['id'],
        name: src['name'],
        age: src['age'],
        createdAt: DateTime.parse(src['createdAt']),
      );

  @override
  Map<String, dynamic> toDict() => {
        'id': id,
        'name': name,
        'age': age,
        'createdAt': createdAt.toUtc().toIso8601String(),
      };

  @override
  User clone() {
    return User.fromDict(toDict());
  }
}

void main() {
  final db = DeltaTraceDatabase();
  final now = DateTime.now();

  // サンプルデータを追加
  final addQuery = QueryBuilder.add(
    target: 'users',
    addData: [
      User(id: -1, name: "Taro", age: 30, createdAt: now),
      User(id: -1, name: "Jiro", age: 25, createdAt: now),
      User(id: -1, name: "Saburo", age: 20, createdAt: now),
    ],
    serialKey: "id",
    returnData: true,
  ).build();
  db.executeQuery(addQuery);

  // 名前にroを含む者を検索
  final searchQuery = QueryBuilder.search(
    target: 'users',
    queryNode: FieldContains("name", "ro"),
    sortObj: SingleSort(field: 'age', reversed: true), // 年齢・降順
    limit: 1, // 1件だけ取得する
  ).build();

  final searchResult = db.executeQuery<User>(searchQuery);
  final matchedUsers = searchResult.convert(User.fromDict);
  print(matchedUsers.first.name); // → "Taro"
  print(searchResult.toDict());

  // ページング機能で以前の検索結果の続きから取得する。
  final pagingQuery = QueryBuilder.search(
    target: 'users',
    queryNode: FieldContains("name", "ro"),
    sortObj: SingleSort(field: 'age', reversed: true),
    limit: 1,
    startAfter: searchResult.result.last, // 以前の結果の最後を指定
  ).build();

  final nextPageSearchResult = db.executeQuery<User>(pagingQuery);
  // クラスに直接変換できます。
  final nextPageUsers = nextPageSearchResult.convert(User.fromDict);
  print(nextPageUsers.first.name); // → "Jiro"
  print(nextPageSearchResult.toDict());

  // 最初の1件だけ欲しい場合に利用できる高速検索
  final searchOneQuery = QueryBuilder.searchOne(
    target: 'users',
    queryNode: FieldEquals("id", 0),
  ).build();

  final searchOneResult = db.executeQuery<User>(searchOneQuery);
  print(searchOneResult.result.first["name"]); // → "Taro"
  print(searchOneResult.toDict());
}

サンプルの通り、特別な環境構築は不要です。
import → 使う だけです。

クエリはtoDict()すればJSONとして保存・転送できるため、バックエンドとの同期もシンプルに実装できます。

より詳しい使い方はドキュメントを参照してください。
クエリは以下のAIプロンプトを使って生成することもできます。


なぜ作ったのか

社内で開発をしていると、DB周りでこういう問題がたまに起きます。

  • 既存のDBがプロジェクトの要件に合わない
  • SQL設計やスキーマ変更が面倒
  • バグやAIの誤動作でDBを壊してしまい、調整が大変になる

そこで思いました。

「クエリ(操作)をすべて履歴として保存すれば、いつでも戻せるし安全では?」

ということで、このDBができました。


他のDBとの違い

DB 特徴
SQLite 組み込みSQL DB
Redis 高速インメモリKV
MongoDB ドキュメントDB
DeltaTraceDB 履歴管理 + 巻き戻し可能DB

DeltaTraceDBは 「すべての操作を履歴(Delta)として積み上げる」設計のDBです。

スナップショットを読み込み、その後に発行された保存済みDelta(クエリ)を順に実行することで、任意時点のDB状態を完全に復元できます。


設計のポイント

1. NoSQL

オブジェクトをそのまま保存できます。全文検索を標準サポートしているため、NoSQLでありがちな「検索が弱い」という問題にも対応しています。

2. インメモリDB

ディスクアクセスがないため非常に高速です。
最近の環境なら数万〜10万レコード程度はメモリ上で快適に扱えます。
※ データはメモリ上にのみ存在するため、永続化は自分で行う必要があります。

3. Deltaログ

すべての操作はクエリをシリアライズした「Delta(差分)」として扱われます。
これを使用感として例えるなら、**「DB版のGit」**です。
※ 各操作が「コミット」のように履歴として積み上がるイメージです(ブランチ機能はありません)


データの永続化とセキュリティ

DeltaTraceDBは、ライブラリ側で勝手にファイル保存を行いません。
「DBの状態をどう保存するか」を開発者が完全にコントロールできるのが特徴です。

メモリ上で暗号化してから保存

「DBの中身をそのままディスクに置きたくない」という場合でも、メモリ上で好きな方式で暗号化してから好きな場所に保存できます。

// 1. スナップショット(現在の全データ)を取得
final rawData = db.toDict();

// 2. メモリ上で暗号化
final encrypted = myEncrypt(rawData); 

// 3. 好きなストレージ(ファイル、Cloud Storage等)に保存
await saveToFile(encrypted);

クエリ自体がDeltaデータなので、**「最新のスナップショット + それ以降に発行したクエリログ」**を保存しておくだけで、
最高精度のデータ復元と機密性を両立できます。


AI時代のDB:詳細なメタデータ保持(Cause)

AIエージェントがデータを操作する時代、**「なぜその操作が行われたか」**の追跡が不可欠です。
DeltaTraceDBでは、クエリに Cause(原因)を付与できます。

final cause = Cause(
  who: Actor(
    EnumActorType.human, // human, ai, systemが選べる。
    "user_id_001",
    // 実行時にどの権限に基づいて操作されたかも記録可能
    // ただしこれは、ユーザーサイドからのメモ的なデータであり、
    // 検証にはサーバー側データを使う必要がある。
    collectionPermissions: {
      "users": Permission([EnumQueryType.add, EnumQueryType.update]),
    },
  ),
  // 通信経路や時系列の追跡にも対応しています。
  when: TemporalTrace(
    nodes: [
      TimestampNode(
        timestamp: DateTime.now().toUtc(),
        location: "Frontend App (Tokyo)",
      ),
    ],
  ),
  what: "Update user profile",
  why: "User clicked the save button", // 変更の意図を言語化して保存
  from: "Flutter Web",
);

// クエリ作成時に cause を渡す
final query = QueryBuilder.add(
  target: 'users',
  addData: [newUserData],
  cause: cause, // ここで紐付け
).build();

AIが暴走しても「どのAIが、いつ、何の意図で書き換えたか」がログに残るため、容易に特定と復元が可能です。


向いている用途

特に向いているのは

  • 個人データ管理(機密性が高いもの)
  • AIエージェントによるデータ操作
  • 中小規模の業務システム
  • アプリのローカルDB

向いていない用途

インメモリDBなので

  • 巨大EC
  • 超大規模トラフィック

などのメインDB用途には向いていません。
速度も、超大規模になってくると、専用の探索アルゴリズムがあるDBの方が良いです。


まとめ

DeltaTraceDBは 「データを壊すのが怖い」 という開発者のストレスを大幅に軽減できます。
そして、もし問題が起きても、追跡がかなりしやすいです。

  • シンプル: インポートするだけ
  • 高速: インメモリ
  • 安全: delta履歴 + 開発者による自由な暗号化保存

なおインメモリなので、クラウド関数で使って維持コストを格安に抑えるといった使い方もできます。
コスト削減方面でも使い勝手が良いので、社内では現在色々な使い方を模索中です。


おわりに

もし業務での活用や機能追加の要望等があれば、私の会社で日本語でのサポートも可能です。
(というか、国産のDBはもっと出てきても良いと思うんですよね。システムのコアの部分ですし)

興味があればぜひ触ってみてください。最小サンプルをコピペするだけで普通に動きます。

では、良きコーディングを。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?