Files

88 lines
3.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""JSON 存储引擎:一个文件 = 一张表。
当前阶段用本地 JSON 文件代替数据库。每个文件是一个 JSON 数组,
元素为一行记录;存储层只做最朴素的"读全量/写全量 + 原子替换"
业务逻辑全部收敛在 ``repositories.py``。
未来数据库映射(下一阶段据此设计):
JsonTable(path) -> 一张数据库表(表名 = 文件名)
数组元素 -> 一行记录(主键 = 记录里的 ``id`` 字段)
记录字段 -> 一列
"""
from __future__ import annotations
import json
import threading
from pathlib import Path
from typing import Callable
class JsonTable:
"""对单个 JSON 数组文件的最小 CRUD 封装。
- 写入使用「写临时文件 + rename 替换」,避免崩溃/并发读到半截数据。
- 内部用 RLock 保证同一进程内的读写互斥(演示级并发足够)。
"""
def __init__(self, path: Path):
self.path = path
self._lock = threading.RLock()
def all(self) -> list[dict]:
"""读全部记录。文件不存在/损坏时返回空表(下次写入时重建)。"""
with self._lock:
if not self.path.exists():
return []
try:
with open(self.path, "r", encoding="utf-8") as fh:
data = json.load(fh)
except (json.JSONDecodeError, OSError):
return []
return data if isinstance(data, list) else []
def save(self, records: list[dict]) -> None:
"""整体写回。原子:先写 ``<name>.tmp`` 再 rename。"""
with self._lock:
self.path.parent.mkdir(parents=True, exist_ok=True)
tmp = self.path.with_name(self.path.name + ".tmp")
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(records, fh, ensure_ascii=False, indent=2)
tmp.replace(self.path)
# ------------------------------------------------------------------
# 便捷操作
# ------------------------------------------------------------------
def find(self, predicate: Callable[[dict], bool]) -> dict | None:
"""返回第一条满足条件的记录,没有则 None。"""
for record in self.all():
if predicate(record):
return record
return None
def insert(self, record: dict) -> dict:
"""追加一条记录并返回它。"""
records = self.all()
records.append(record)
self.save(records)
return record
def update(self, record_id: str, fields: dict) -> dict | None:
"""按 ``id`` 合并更新字段,返回更新后的记录(不存在则 None)。"""
records = self.all()
for record in records:
if record.get("id") == record_id:
record.update(fields)
self.save(records)
return record
return None
def delete_matching(self, predicate: Callable[[dict], bool]) -> int:
"""删除所有满足条件的记录,返回删除条数。"""
records = self.all()
kept = [r for r in records if not predicate(r)]
removed = len(records) - len(kept)
if removed:
self.save(kept)
return removed