Project

jsondb-rb

0.0
The project is in a healthy, maintained state
jsondb 将单个 JSON 文件作为嵌入式文档数据库:集合/文档 CRUD、点路径与查询操作符($gt/$in/$regex/$or...)、链式 where/order/limit、更新操作符($set/$inc/$push/$pull)、事务与原子落盘。纯 Ruby 标准库,无运行时依赖。
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

jsondb-rb

CI

把一个 JSON 文件当数据库读写的 Ruby 库。零运行时依赖(纯标准库),MongoDB-lite 查询语法,落盘永远原子。

gem 名 jsondb-rb(rubygems 的 jsondb 已被 2014 年旧库占用);require 路径与模块名不变:require 'jsondb' → JsonDb。

require 'jsondb'

db = JsonDb.open('app.json')
users = db[:users]

users.insert(name: 'Ann', age: 31, tags: %w[ruby db])
users.insert_many([{ name: 'Bob', age: 17 }, { name: 'Cara', age: 45 }])

users.where(age: { gte: 18 }).order(:name).limit(10).to_a
users.where('profile.city' => 'SZ', age: (18..60)).count
users.where('$or' => [{ vip: true }, { 'score' => { '$gte' => 90 } }]).pluck(:name)

users.update_many({ age: { '$lt' => 18 } }, '$set' => { 'minor' => true })
users.upsert({ name: 'Ann' }, '$inc' => { 'login_count' => 1 })

db.transaction do
  db[:orders].insert(user: 'Ann', total: 99)
  db[:users].update_one({ name: 'Ann' }, '$push' => { 'orders' => 'o-1' })
end # 一次落盘;块内抛错自动回滚

db.close

数据文件长什么样,人可以直接编辑(改完下次读取自动重载):

{
  "users": [
    { "name": "Ann", "age": 31, "_id": "5f3a…" }
  ]
}

设计

  • 单文件:顶层对象,键 = 集合名,值 = 文档数组。备份就是复制文件。
  • 内存权威:读写都走内存;写操作后默认即时落盘(autoflush: true)。
  • 原子落盘:同目录临时文件 → fsync → rename → 目录 fsync。任何时刻磁盘上只有完整的旧文件或新文件,绝无半截 JSON。
  • _id:文档唯一键;插入时缺省自动生成 24 位 hex。可自定义(insert(_id: 'u1', …)),不可变更。
  • 键与值的字符串化:symbol 键/值入库时深度转为字符串(name: :active ↔ "active"),保证 JSON 往返一致;查询过滤器里的 symbol 自动同样处理。

查询

链式:where(...).where(...)(AND 叠加)order / limit / offset / pluck / first / count / exists?,惰性执行,枚举时取快照。

字段名支持点路径,含数组下标:'profile.city'、'tags.0'。

操作符支持两种写法:字符串 '$gt' 与裸符号 gt:(age: { gte: 18 })。显式 $ 前缀写错操作符名会抛错;裸符号不在操作符表内则按数据 Hash 等值匹配。

过滤写法 含义
age: 31 等值
age: { '$gt' => 18 }($gte $lt $lte 同理) 比较;缺失/类型不合不匹配、不抛错
age: (18..60) 区间(Range)
name: { '$ne' => 'Ann' } 不等(缺失字段也算匹配)
age: { '$in' => [18, 31] } / '$nin' 枚举(元素可为 Regexp)
active: { '$exists' => true } 键存在性(值为 false 也算存在)
name: { '$regex' => /^A/ } 正则(String 或 Regexp)
tags: { '$size' => 2 } / '$all' => [...] 数组长度 / 全包含
age: { '$type' => 'integer' } JSON 类型(null/boolean/integer/float/string/array/object)
age: { '$not' => { '$gte' => 18 } } 字段级取反
'$and' / '$or' / '$nor' 逻辑组合(空 $or 永不匹配)
name: /^A/(直接 Regexp 值) 等价 $regex

排序对混合类型安全(false < true < 数值 < 字符串 < 数组 < 对象 < nil,nil/缺失垫底;平键保持插入序)。order(:name)、order(:age, :desc)、order(age: :desc, name: :asc) 均可,多次调用逐键追加。

更新

写法 含义
update_one(f, { age: 32 }) 裸 Hash = 按字段赋值(值整体替换)
'$set' => { 'a.b' => v } 点路径赋值,中间缺失自动补 {}
'$unset' => { 'a.b' => '' }(或数组) 删键
'$inc' => { 'n' => 1 } 数值增减(缺失视 0;目标非数值抛错,整批不落地)
'$push' => { 'tags' => v } / { '$each' => [...] } 追加数组元素
'$pull' => { 'tags' => v } 删等值元素;Hash 值按子过滤器删

update_one / update_many(批量)、update(id, changes)(按 _id)、upsert(filter, changes)(无则插:过滤条件 ⊕ 更新集合成文档)。操作符中途失败时该次调用全部不落地(先在副本上应用成功再写入)。

删除:delete(id) / delete_one(filter) / delete_many(filter) / clear!;db.drop(:name) 连集合一起删。

持久化与事务

  • JsonDb.open(path, pretty: true, autoflush: true);pretty: false 出紧凑 JSON。
  • 批量写入:JsonDb.open(path, autoflush: false) 后手动 db.flush;或用 db.transaction { … }(块内禁即时落盘,正常结束一次落盘,抛错整体回滚到事务前)。
  • db.close 落盘并封存实例;db.reload! 丢弃内存改动强制重读。
  • 外部改动:无未落盘改动时,下次读取自动察觉(mtime+size)并重载——手编文件、多进程读都新鲜。本地有未落盘改动且文件又被他方改写时,打印一次警告,落盘以本地为准。

并发语义(如实声明)

  • 单进程多线程:安全(Monitor 串行化,含 fork 前后语义)。
  • 跨进程:写经数据文件旁的 <path>.lock flock 串行化,落盘原子;多进程并发写为文件级 last-write-wins,不合并——两进程同时各自写不同文档,后落盘者覆盖整文件。写者之间靠 mtime 重载尽量累积对方改动,但读-改-写竞态窗口内的丢失不设防。需要强一致多写,请单写者或外部协调(这也是同类库 lowdb/TinyDB 的共同边界)。
  • 读永远一致:任何进程任何时刻读文件,只会看到完整的某一版。

约束与边界

  • 文档只支持 JSON 原生类型(Hash/Array/String/Numeric/Boolean/nil);Date/Time 等请自行转字符串。symbol 自动转字符串。
  • 无索引:每次查询全集合扫描。万级文档以下舒适,十万级请上真数据库。
  • $elemMatch、投影(projection)、聚合:暂无,见需再加。

性能

复现:rake bench(微基准 ips)/ rake bench[scale](规模表)。脚本在 benchmark/,数据为本机实测(Ruby 4.0.6, arm64 M 系列 MacBook,2026-10)。

缩放表(预填后 autoflush 关闭测内存操作,flush 单列;查询无索引全扫描):

操作 1 000 文档 5 000 文档 20 000 文档
reload(整库读+解析) 0.70 ms 6.9 ms 17.0 ms
等值查询(命中 1%) 3.1 ms 9.1 ms 50.8 ms
范围查询 2.4 ms 13.0 ms 61.9 ms
排序 + limit 10 10.8 ms 66.8 ms 352.8 ms
count(带过滤) 1.9 ms 7.8 ms 33.8 ms
insert ×100(内存) 6.3 ms 28.7 ms 158.0 ms
update ×100($inc,未命中全扫) 174 ms 826 ms 3 388 ms
flush(全量原子落盘) 1.7 ms 4.9 ms 13.9 ms
数据文件大小 235 KB 1.2 MB 4.8 MB

单操作亮点(1 000 文档,微基准):

操作 吞吐 单次
update by _id(命中首条即止) 44 000/s 23 µs
docs 快照 295 万/s 339 ns
insert(内存,小集合) ~2 700/s 371 µs
insert(原子落盘) ~510/s 1.95 ms
update(原子落盘) ~460/s 2.18 ms
reload 1k 文档 1 460/s 684 µs

读数要点(诚实边界):

  • 扫描吞吐约 50–60 万文档/秒(无索引线性扫,与集合规模基本无关的单文档成本 ~2–3 µs)
  • 每次落盘 ≈ 1.9–2.2 ms(tempfile+fsync+rename+目录 fsync 的真实代价);高频写建议 autoflush: false 批量 flush,或 transaction 合并
  • update/delete 按过滤条件为全扫(未命中尤甚,上表 update 行即最坏情形);按 _id 命中快
  • 排序为比较器全排(20k 文档 ~350 ms),键已预计算(装饰-排序-去装饰);跨类型安全比较是刻意设计,换取任意 schema 不抛错
  • 万级文档舒适,十万级且高频查询请上真索引型数据库

安装

# Gemfile(gem 名 jsondb-rb,require 路径 jsondb)
gem 'jsondb-rb', '~> 0.1'
gem install jsondb-rb   # 然后 require 'jsondb'

发版走 git tag(v*)触发 release.yml,经 rubygems OIDC trusted publishing 推送。

测试

rake test   # 44 例:CRUD / 查询操作符 / 持久化 / 事务 / 多线程 / 多进程 fork 并发

Ruby >= 3.1,纯标准库。