Приветствую!
Я хочу вам рассказать о моей разработке — JSON БД на Python
Не все проекты нуждаются в медленных и сложных SQL базах данных, например ТГ‑Боты и парсеры. Конечно не буду спорить, что SQL — вероятно лучшее решение для бизнеса, но иногда они могут быть избыточными для небольших проектов. NoSQL базы данных — это отличный выбор для таких случаев.
Если вы уже знакомы с MongoDB или Redis, то принцип работы моей библиотеки вам будет понятен.
Ну что, начнём?
Основы
from jsoner import Database
# инициализация объекта БД
db = Database('db.json')
# добавление данных
db.add('key', 'value')
# получение
db.get('key') # 'value'
# запись изменений в файл
db.commit()
# изменение значения
db.update('key', 100)
# увеличение значения на 5
db.incr(key, 5)
# уменьшение значения на 15
db.decr(key, 15)
# удаление
db.delete('key')
Теги
Теги создаются путем добавления словаря из пар Тег: значение
from jsoner import Database
from jsoner.tags import const_tag
from jsoner.errors import ValueIsConstant
db = Database('db.json', autocommit=True)
# тег неизменяемого значения
db.add('pi', 3.14, {const_tag: True})
try:
db.update('pi', 4)
except ValueIsConstant:
print('Ключ "pi" - константа')
>>> 'Ключ "pi" - константа'
Теги и их значения записываются в словарь tags
db.json
{
"__settings__": {
"__version__": "0.1",
"default": null,
"tags": {
"pi": {
"const": true
}
},
"global_tags": {}
},
"pi": 3.14
}
Создание своих тегов
С помощью класса NewTag
можно создавать свои теги
from jsoner import Database
from jsoner.tags import NewTag
db = Database('db.json')
class MyTag(NewTag):
# создании тега, метод должен вернуть преобразованный аргумент тега
def create(db: Database, value, tag_arg):
return tag_arg
# изменение значения
def update(db: Database, key: str, old_value, new_value, tag_arg):
return new_value
# чтение значения
def read(db: Database, key: str, value, tag_arg):
return value
Пример создания своего тега
from jsoner import Database
from jsoner.tags import NewTag
import time
class ttl_tag(NewTag):
'Time to life - время жизни ключа'
def create(db: Database, value, tag_arg) -> str:
# tag_arg в данном случае - время действия ключа
# в аргументе тега сохранится время, до которого действителен ключ
return tag_arg + time.time()
def read(db: Database, key, value, tag_arg):
# если текущее время больше аргумента тега, ключ следует удалить, и вернуть значение по умолчанию
if time.time() <= tag_arg:
return value
else:
db.delete(key)
return db.data[db.settings]['default'] # тут хранится значение по умолчанию
Установка значения по умолчанию
from jsoner import Database
db = Database('data.json')
# установка значения по умолчанию
db.set_default(0)
print(db.get('Unknown key'))
>>> 0
Добавление глобальных тегов
Глобальные теги действительны и одинаковы для всех ключей
from jsoner import Database
from jsoner.tags import const_tag
from jsoner.errors import ValueIsConstant
db = Database('data.json')
# добавление глобального тега
db.set_global_tag(const_tag, True)
db.add('num', 123)
db.update('num', 0)
>>> raise ValueIsConstant
Работа с оператором with
При входе в оператор with
вызывается метод db.discard()
, который стирает несохраненные в файл данные
Если атрибут autocommit
у db
равен True
, то он заменится на False
, а при выходе из with
вернется обратно
При выходе из with
вызывается метод db.commit()
from jsoner import Database
db = Database('data.json')
db.add('num', 0)
with db:
db.add('key', 'value')
print(db.items())
>>> [('key', 'value')]
Другие полезные методы
discard - стереть несохраненные в файле данные
get_many - получить несколько значений по ключам
set - автоматически либо добавляет, либо изменяет данные. Т. к. при добавлении существующего ключа или обновлении несуществующего вызовется исключение
keys, values, items - схожи с методами из обычных словарей
db[
'key'
] ='value'
- установить значениеdb[
'key'
] - прочитать значение
Полное описание и код можете увидеть на моём гитхабе
Послесловие
Приму конструктивную критику в свой адрес, но дяденьки-программисты, хочу вам сказать, что мне 15 годиков, так что не судите строго
Комментарии (14)
fire64
06.08.2024 09:24+4А чем sqlite3 не угодил, если хотите использовать файловое решение и встроенную BD?
Andrey_Solomatin
06.08.2024 09:24+3В статье нет сравнения с аналогами. А их много. Например diskcache.
SQL тоже может быть лёгким, например sqlite.
Не плохо бы замутить какое-нибудь сравнение по производительности.remzalp
06.08.2024 09:24+2Случай, когда комментарий делает статью в разы лучше :)
https://pypi.org/project/diskcache/ - раньше не встречал, но и не искал
sqlite3 хватаетAndrey_Solomatin
06.08.2024 09:24У меня скриптик скачивает джейсоны и потом их процессит, diskcashe для этого удобнее, так как не надо менять формат данных. А sqlite там внутри работает.
little-brother
06.08.2024 09:24+6Одна из проблем текстовых форматов хранения, таких как json, XML или csv в том, что при изменении хотя бы одного символа в середине вам требуется переписывать весь файл на диске, который и так являются узким местом. Базы данных, как и файловые системы, работают с блоками данных и при сохранении изменения вносятся только в них.
в медленных и сложных SQL базах данных
Так что медленным будет именно ваше решение. А сложность - это понятие относительное. Реализовать json-базу данных можно и на SQLite. Все необходимое есть уже из коробки: не только функции для работы с данными по пути, но и даже индексация и бинарное хранение для экономии места/времени обработки. Но даже у SQLite есть свои ограничения, напр. плохо подходит, когда в базу пишут несколько потоков или невозможность работать с ней по сети.
Использовать Mongo или Posgres для чат-ботов это конечно из пушки по воробьям, но с другой стороны если у вас много ботов, возможно расположенных на разных машинах/контейнерах Docker, то разумно писать всё в одну базу, чтобы иметь возможность собирать по ним статистику, к примеру.
И просто стоит упомянуть, что базы данных - это не только таблицы, но и дополнительные плюшки, такие как консистетность данных, триггеры, индексы, иногда хранимые процедуры, планировщики запросов и различные утилиты. У вашего решения ничего этого нет.
dima-doroshenko Автор
06.08.2024 09:24Консистентность данных и триггеры можно реализовать с помощью глобальных тегов. И зачем нужны хранимые процедуры когда база данных ключ-значение?
little-brother
06.08.2024 09:24Реализовать можно практически что угодно. Другое дело, что обычно хочется чтобы это уже было. Так некоторые отказываются от SQLite, поскольку хранимых в ней нет. С другой стороны, многие проекты используют далеко даже не половину возможностей предоставляемых базой данных, особенно те, которые заявляют поддержку сразу нескольких СУБД.
Лично я бы базу данных реализовать не стал, т. к. проще взять уже готовую СУБД, коих сейчас на любой вкус и цвет, а для маленьких проектов достаточно использовать json напрямую, т. е. загрузить данные в память, обработать их и записать результат в тот же или другой json.
Для JavaScript было что-то подобное вашей базе - NeDB. Проект весьма ожидаемо был свернут, ибо конкурировать за разработчиков на файловых базах данных с SQLite очень сложно: вы должны не просто быть сопоставимы по функционалу, а предложить что-то такое, что SQLite не может, и тогда может быть появится шанс.
Derfirm
06.08.2024 09:24Спасибо за интересный взгляд на работу с жсон/хранилищем для него. Радость и боль жсона в его структуре и ограниченность поддерживаемых типов данных. С одной стороны можно скормить что угодно из любых источников, но как только возникает необходимость отмодифицировать какой-то кусок данных, тотчас находится тысяча и один способ сделать это неэффективно. Начиная от переписывания файла с жсоном при изменении одного значения до втягивания монго с ее пусть редкими но всплывающими ограничениями (речь про in-place update). Например, нужно всегда валидировать тип и диапазон для чисел при вставке/обновлении, всегда таскать жсонсхему в довесок ?
Если жсон как контракт или нечто фиксированное для обмена, то с этим можно жить, а если хочется большего?
CrazyElf
06.08.2024 09:24А у меня другая претензия. Утвердительное предложение, само по себе не несущее негативной коннотации - плохое название для исключения. Ну значение константа - и что? CanNotChangeConstant было бы гораздо понятнее. А просто "значение - константа" - и что? Сразу непонятно что )
Andrey_Solomatin
06.08.2024 09:24Мои настройки линтеров будет ругаться на такое имя. https://docs.astral.sh/ruff/rules/error-suffix-on-exception-name/
И я стараюсь избегать Not в именах.
Может быть: ConstantChangeErrorCrazyElf
06.08.2024 09:24+1Да мне самому моё не очень нравится, но не придумал лучше. Ваше предложение хорошее, одобряю :)
Dominux
06.08.2024 09:24+3медленных и сложных SQL базах данных
NoSQL базы данных — это отличный выбор для таких случаев.
Если вы уже знакомы с MongoDB
Сильное заявление, настолько сильное, что я даже не знаю, где новички берут такие неверные данные. Самое глупое - потом писать об этом всему миру в своих статьях, к сожалению, продвигая ложную информацию далее
Очень много наплодилось новичков в индустрии, которые не так поняли, зачем нужна та же монга и другие NoSQL, и юзают ее без разбора, а потом мучаются с тем, что нет никакой схемы данных. Данные вставить без наличия ограничений - изи, а как потом строить запросы - уже попаболь, о которых маслята, как им и принято, не задумываются заранее!
Если конкретно по монге, то она все же хороша для хранения больших вложенностей разнообразной структуры и глубины и в простоте организации кластера распределенных нод, где не нужно делать шардирование/партиционирование более сложными путями. Ее популяризация в стеках MEAN, MERN, MEVN и тд несколько лет назад, очевидно, и привела к тому, что каждый вкатун так и наравит использовать именно ее из-за простоты использования по сравнению со стандартными РСУБД
TerraV
Больше велосипедов богу велосипедов
Andrey_Solomatin
Не на pypi и то хорошо.
Нормальный учебный проект, автор научится чему-то новому.