Issue #1 open

Custom DB backends (incl. non-relational)

Andy Mikhailenko
created an issue

(напишу по-русски, тк пока не вижу иноязычных участников)

На днях наткнулся на этот фреймворк, выглядит очень заманчиво -- этакий компромисс между джангой и пайлонс.

Действительно, Django имеет отличную документацию и гарантирует бесшовное связывание типичных компонентов-батареек, что очень важно при прототипировании и в целом при работе над сайтом. Но чуть шаг в сторону -- расстрел. Не так, как в CMS, но все же все ключевые компоненты завязаны на ряд ядреных компонентов и друг на друга. Выпилить формы не очень просто, ORM и вовсе бесполезно.

Pylons в этом плане гораздо лучше -- ничего не навязывает. С другой стороны, документация довольно слабенькая, систему нужно практически полностью собирать самостоятельно и самому заботиться об их связывании -- в таком случае возникает вопрос о пользе самого фреймворка. Возможно, для полной свободы лучше взять CherryPy и прицепить к нему то, что нужно (routes, i18n, template engine, ...).

Почему CherryPy не хватает? Потому что при разработке различных сайтов неминуемо начнет вырисовываться некий фреймворк: набор инструментов + конвенция именования и структурирования кода + какой-то клеевой код (glue code). Затем всё это добро выделится в отдельный проект и получится Еще Один Фреймворк.

Итак, чем был бы полезен "не-джанго-и-не-пайлонс": 1) хорошо документированный стандартный набор компонентов с клеевым кодом; 2) поддержка других компонентов -- не путем нагромождения абстракций, а через легкую и легко расширяемую архитектуру.

Мне кажется, это всё применимо к Сварге.

Теперь, собственно, по теме, обозначенной в тикете: мне понадобится SQLAlchemy только в одном проекте, для всего остального хотел бы использовать Tokyo Tyrant (через pyrant). Каким образом мне следует подключать к фреймворку документо-ориентированный бэкенд: а) через внешнюю библиотеку моделей, б) путем модификации svarga.models в своем форке, или в) путем расширения svarga.models миксинами, как в svarga.contrib.appengine? Или есть еще какие-то варианты? Интересно, насколько этот аспект вообще продуман в svarga.

Comments (28)

  1. Serge Koval

    Я постараюсь рассказать что мы хотели добиться моделями и их заменяемостью. Ну и в целом, немного о ахритектуре клея.

    В сварге все заменямо. Например модели это подключаемый кусок кода (см. svarga.core.conf.local_settings).

    Модели "сквозные" - если препроцессор моделей находит понятный ему класс-маркер (а-ля StringColumn), он знает как его превратить в нужный тип. Если нет - оставляет как есть. В результате, разработчик не ограничен синтаксисом ОРМ сварги.

    Если хочется сделать свои модели, то все сводится к:

    1. Сделать провайдер моделей (копируя куски из базового), который ничего не делает кроме как:

    - Регистрирует список моделей из аппликух

    - Генерирует метаданные из моделей

    2. Опционально:

    - Добавить поддержку "общего" синтаксиса моделей сварги

    - Сделать поддержку ModelManager/запросов а-ля алхимия

    Если пункт 2 не будет сделан, модели будут описываться синтаксисом конкретной либы (например - pyrant). Запросы тоже будут делаться через библиотеку, с ее синтаксисом запросов. Минусом такого подхода - другие аппликухи могут не взлететь с такими моделями. Пример - svarga.contrib.sessions (при бекенде db) и svarga.contrib.user. Это решаемо написанием бекендов для сессий и юзеров.

    На текущий момент, поддержка GAE - unstable. Относительно недавно были добавлены метаданные для моделей, бекенд GAE их пока не генерит.

  2. Andy Mikhailenko reporter

    Спасибо, сложилось впечатление, что это the right thing. Правда, в коде все-таки не получается разобраться сходу без подробной документации. Но попробую.

    Пока такой вопрос. Насколько я понял, бэкенд для данных определяется в настройках проекта:

    APPS_INIT = ['my_backend.apps.contribute']
    

    Значит ли это, что нельзя в одном проекте использовать разные базы данных? В т.ч., как будет интерпретироваться конфиг, приведенный ниже?

    APPS_INIT = [
        'svarga.models.apps.contribute',
        'svarga.contrib.appengine.models.apps.contribute'
    ]
    

    И еще вопрос: почему в моделях всех демо-приложений базовый класс генерится функцией factory, которая импортируется из svarga.models.model, но определяется бэкендом и присваивается модулю как атрибут(!) -- это временное решение или плод длительных размышлений? Не лучше ли как-то иначе добывать базовый класс, чтобы снизить удельный вес волшебства в процессе? Хотя бы что-то такое:

    from svarga.conf import settings
    Model = settings.db_backend.get_model_class()    # или ...db_backend.Model
    class Foo(Model):
        ...
    

    Не так кратко, но а) понятно, откуда что берется, и б) не возникает волшебных глобальных переменных. Правда, это не поможет в одном приложении объявить модели с разными бэкендами, но не знаю, насколько это вообще нужно делать там -- скорее во вьюшках что-то вроде Person.objects.using('some_db').all(), как в Django 1.2, но тогда получается, что надо не BaseModel формировать в бэкенде, а лишь конкретные операции в нем обрабатывать, и тогда возникает занятный вопрос насчет метаданных... но, наверно, что-то можно придумать.

    Надеюсь, я просто чего-то не заметил и на самом деле всё проще. :)

  3. Serge Koval

    Попробую на все ответить :-)

    1. Мы не предполагали что может быть два провайдера моделей паралельно. В принципе, этому почти ничего не мешает, разве что Exception'ы ловить будет неудобно. Пример - кинула алхимия NotFoundException (или как там его), ловить надо будет конкретный тип. Сейчас провайдер моделей закидывает в svarga.models.model некоторые, наиболее используемые, эксепшены.

    Такой вариант решения задачи мне не нравится:

    from svarga.models import get_exception
    
    NotFoundException = get_exception(NotFoundException)
    
    NotFoundException = get_exception(NotFoundException, 'sqla')
    

    А хотелось как раз унификации и избавления от лишних импортов.

    Возможна, конечно, альтернатива вида:

    class MyModel(Model):
        ...
    
    try:
        q = MyModel.objects.get(10)
    catch MyModel.objects.NotFoundException:
        pass
    

    Вроде и выглядит неплохо, и импортов нет, но текста писать прийдется больше.

    2. factory, на самом деле - наследие прошлого. В алхимии, все модели которые declarative, создаются вот через их factory. Cм.: http://www.sqlalchemy.org/docs/05/reference/ext/declarative.html Конечно, это самое factory можно хранить и не как глобальную переменную, но и не в настройках. Лучше сделать что-то типа:

    from svarga.models import factory
    
    # Default model provider
    Model = factory()
    
    # Non-default model provider
    Model = factory('sqla')
    

    Но это потребует некоторого переписывания текущей ахритектуры моделей, однако изменения не такие уж и серьезные. Factory переименовать тоже можно, но оно вроде и так нормально выглядит.

    Переключение с запросом между бекендами (...using('some_db')...) - нереально и, наверное так делать не стоит. Модели они такие - незаменяемые. Если модель была создана для GAE, использовать ее для алхимии не получится, как ни крути. Для нас "общий" синтаксис моделей это просто синтаксический сахар, который преобразуется под конкретный тип модели конкретного бекенда моделей. В связи с этим, метаданные генеряется так, как умеет их генерить конкретный бекенд.

  4. Alexander Solovyov repo owner

    Бтв, мне идея с `except Model.NotFound` кажется достаточно удачной - и в джанге она используется, и количество импортов уменьшается.

  5. Andy Mikhailenko reporter

    Терять или перетаскивать Model.NotFound в менеджер не хотелось бы, ага. Но почему бы не ловить в методах менеджера backend.NOT_FOUND_CLASS и не выбрасывать models.Model.NotFound? Естественно, надо позаботиться о сохранении ценной информации.

    По поводу переключения между бэкендами на лету -- я пока вижу лишь проблему с полноценной валидацией полей в момент их объявления. А всё остальное, по-моему, не так уж сложно вынести за пределы модели.

    (Сразу оговорюсь: я не предлагаю курочить Сваргу, чтоб винтики летели во все стороны. Просто хочется понять, есть ли грабли, или просто вы подошли к вопросу с другой стороны, с другими приоритетами.)

    Модель связана с БД в двух точках(?): "питонизация" и "депитонизация" данных. Оба процесса подразумевают знание питоньего типа (определен в модели) и соответствующего ему типа в конкретной базе данных (определяется бэкендом). Возьмем такой вот кусочек кода (с минимальным синтаксахаром, без привязки к Сварге):

    from framework.models import Model    # стандартный универсальный класс
    
    class Foo(Model):
        date = models.fields.DateField()          # стандартное поле
        info = mongo_backend.fields.DictField()   # поле, специфичное для бэкенда
    

    При сохранении:

    1. date, стандартное поле:
      1. запрашиваем у бэкенда обработчик (валидатор/конвертер) для DateField; если найден -- берем его, если нет -- берем умолчальный из DateField;
      2. получаем "депитонированное" значение через обработчик, сообщая ему данные, определенные при инициализации поля.
    2. info, специфичное для бэкенда поле:
      1. если бэкенд соответствует текущему, получаем "депитонированное" значение стандартным образом;
      2. если бэкенд не соответствует текущему:
        1. если поле обязательное, вываливаемся с ошибкой;
        2. если поле факультативное, игнорируем его.

    Еще одна проблема, связанная с using(storage)/save(storage) -- битые или неоднозначные ссылки между моделями. Но это уже проблема не фреймворка, а клиентского кода. В идеале, конечно, надо сохранять namespace вместе с идентификатором объекта, но реляционные базы замкнуты в себе и не поддерживают "по-настоящему внешних" ключей. В document-oriented, действительно, можно и полный URI держать, так что смена хранилища может протекать безболезненно -- при условии нейтральности моделей.

    Подчеркну, что эти измышления -- не столько предложение изменений, сколько поиск граблей. В частности, я не работал с Алхимией и не знаю, может ли вообще такая вот штука с ней работать без потери какого-то существенного функционала.

  6. Serge Koval

    Ловить эксепшн как-то не хочется: мы же запрос не выполняем сразу, а просто форвардим его в соответсвующий бекенд (алхимию, например). Если там прийдется ловить еще и исключение, то мы должны все-все оборачивать через наш класс. Пример:

    q = MyModel.objects.filter_by(a=10)
    q = q.filter_by(b=20)
    q = q.all()
    

    Переключение между бекендами - слишком много нашего слоя получается и он однозначно будет толстым. Чего очень хотелось избежать. Плюс, пока в голову не приходит use case где такое может вообще понадобиться. Чего хотели "общей" прослойкой - что бы один и тот же код работал на разных бекендах без переписывания, но во время своей работы - всегда с одним бекендом.

  7. Alexander Solovyov repo owner

    Переключение между бекендами мне тоже как-то не очень нравится... Это тупо дофига работы на написание и поддержку, и вообще реальное усложнение логики; а у нас не так и много рабочей силы. ;-)

  8. svet

    А если будет удобный API для подключения необходимого бекенда? Чтобы любой желающий мог подключить свой. И вам меньше работы.

  9. Andy Mikhailenko reporter

    Так API-то, вроде, есть уже? Недавно Сергей вынес Алхимию из ядра модели в бэкенд (если я правильно понял), так что можно взять да скопировать этот самый бэкенд, правя под нужную библиотеку.

    Вопрос скорее в том, что такое "удобный" API и насколько нейтрален имеющийся. Например, он во многом повторяет Алхимию. Как называются классы и методы -- вопрос вкуса, а вот принципы работы могут не состыковаться с бэкендом. Например, я пока продолжаю работу над PyModels, потому что:

    а) в PyModels я могу использовать "естественную идентификацию" моделей (модель=запрос), а в сваргиных моделях, похоже, только унаследованную из RDBMS (модель=таблица). Значит, основные плюсы бессхемного хранилища игнорируются. В самом деле, если модель соответствует жестко указанному пространству имен и описывает (в любом случае) жестко заданный набор полей, зачем отказываться от RDBMS? А вот если модель описывает некоторое подмножество разнородных документов, хранящихся в одном пространстве имен, то она более чем применима к такому хранилищу. Возможно, я ошибаюсь и сваргины модели ничего не навязывают. Тогда супер.

    б) сваргины модели завязаны на Сваргу, а зачастую нужны именно модели, а не весь веб-фреймворк, либо нужен какой-то другой фреймворк (уж честно скажем, не бывает идеального); это отчасти станет не проблемой, а пожеланием, если к Сварге появится исчерпывающая документация (хотя бы в docstrings). Но к API это уже не относится.

  10. Andy Mikhailenko reporter

    тут подумалось -- сопоставление модели сущностям в БД можно тоже оставить на совести бэкенда.

    что идентифицирует запись как принадлежащую к определенной модели:

    1. RDBMS -- имя таблицы;
    2. CouchDB -- database или служебное поле;
    3. MongoDB -- database (вряд ли) или collection или служебное поле;
    4. Tokyo -- только служебное поле.

    то есть, в любом случае сопоставление питономодели чему-то в БД различно для разных бэкендов. я не смотрел, как оно в GAE; мейби вы уже что-то подобное и сделали.

    а поскольку идея засорения данных отсылками к коду (т.е. поля вида "_type=user") мне представляется негодной, было бы разумно в schemaless тот подход из PyModels и употребить, т.е. содержание модели считать шаблоном/запросом, по которому ищутся записи для этой модели.

    при автоматическом сопоставлении (без __table__="x" и must_have={"x__exists":True}) мы угадываем метаданные; для RDBMS это имя таблицы из имени класса, а для schemaless могут быть либо имена-значения служебных полей (тоже из имени класса), либо запрос к базе исходя из списка обязательных полей. помечено поле как обязательное -- модели соответствуют документы с такими полями.

    тогда модель без каких-либо явно указанных метаданных будет работать и с реляционными, и с бессхемными базами, включая такие, где в принципе отсутствует понятие коллекции, таблицы или какого-либо еще уровня организации записей кроме хранилища как такового.

  11. Serge Koval

    GAE уже предоставляет некую ОРМку, которая делалась для разработчиков джанги. Мы просто поверх нее добавили синтаксического сахара.

    table="x" это вообще кусок из sqlalchemy.ext.declarative, нам он не нужен. Если очень хочется, для моделей алхимии можно это поле переопределить, и бекенд алхимии это увидит и не создаст имя самостоятельно. В ГАЕ имя создается программно.

    а поскольку идея засорения данных отсылками к коду (т.е. поля вида "_type=user") мне представляется негодной, было бы разумно в schemaless тот подход из PyModels и употребить, т.е. содержание модели считать шаблоном/запросом, по которому ищутся записи для этой модели.

    Не совсем понял. А зачем? Все что могут делать такие вот поля - брать некоторое строковое значение и попытаться его сконвертировать в нужный тип. Если не смогло - что-то делать, например или кидать исключение или тихо игнорировать. Скорее второе, так как при изменении типов данных, могут быть проблемы.

    А вот все остальное не понял, либо не полностью. Текущий код бекенда подразумевает наличие реляционности на уровне моделей (FK, M2M). Возможно это предположение неправильное и не подойдет для всех вариантов баз данных. Для GAE - подошло.

  12. Andy Mikhailenko reporter

    Текущий код бекенда подразумевает наличие реляционности на уровне моделей (FK, M2M).

    С этим всё в порядке, я не вижу смысла в ином варианте.

    Про __tablename__ -- я имел в виду не именно эту переменную, а в целом связь между моделью (классом в Python) с подмножеством записей в БД.

    Как SQLAlchemy, так и GAE (видимо) подразумевают, что каждой модели соответствуют записи из одноименной таблицы. Это можно переопределить, да.

    В бессхемном же хранилище (CouchDB, MongoDB, Tokyo Cabinet, shelve) таблиц обычно нет (GAE -- исключение, видимо). Там могут быть либо какие-то нейтральные коллекции, либо вообще ничего. Значит, надо как-то иначе цеплять модель к записям. Я и описал возможные способы. К сожалению, мне трудно понять, насколько легко эти способы реализовать в сваргомоделях, потому что в svarga.db.* пока не очень много докстрингов и комментариев, а в документации вопрос освещен с точки зрения разработчика приложений, а не бэкендов. Несмотря на существование двух бэкендов, я не очень представляю, как подступиться к написанию своего.

  13. Artem Semenov

    В бессхемном же хранилище (CouchDB, MongoDB, Tokyo Cabinet, shelve) таблиц обычно нет

    Что значит "бессхемное"? Впервые вижу, чтобы NoSQL так называли :) Схема есть и еще какая. Тот же MongoDB внешне похож на RDBMS. Конкретно, collection == table.

    В случае с документо-ориентированными хранилищами (CouchDB, Riak) принято использовать доп.поле а-ля doc_type, например. TC, Redis и прочие key-value хранилища нужны только лишь для, например, хранения сессий, организации pubsub. Не стоит такие вещи использовать как полноценные хранилища и, соответственно, так же не стоят того, чтобы задумываться о решении "как бы проблемы" объектного подхода.

    Теперь о CouchDB, Riak - тут вопрос совсем другой и данные, в результате отработки MapReduce, могут быть совершенно разные и решения того как можно вогнать абсолютно, так сказать, рандомные данные в модель, лично я не вижу.

    Если подходить к NoSQL с точки зрения моделей, то это не так уж и нужно, ибо они используются для достаточно сложных задач, где даже "самый замечательный орм в мире" просто будет либо адски сложным, либо он тупо не будет существовать вообще.

  14. Andy Mikhailenko reporter

    Что значит "бессхемное"? Впервые вижу, чтобы NoSQL так называли :) Схема есть и еще какая. Тот же MongoDB внешне похож на RDBMS. Конкретно, collection == table.

    Коллекция -- не таблица, потому что таблица определяет структуру, а коллекция не определяет ничего. Коллекция -- просто уровень вложенности. В ней легко могут (и должны) оказаться разнотипные документы.

    "A MongoDB collection is a collection of BSON documents. These documents are usually have the same structure, but this is not a requirement since MongoDB is a schema-free database. You may store a heterogeneous set of documents within a collection, as you do not need predefine the collection's "columns" or fields." (http://www.mongodb.org/display/DOCS/Collections)

    Соответственно, для любого бессхемного хранилища (т.е. хранилища без жестких предопределенных схем записей) необходимо связывать внешнюю сущность с записями через сами данные.

    В случае с документо-ориентированными хранилищами (CouchDB, Riak) принято использовать доп.поле а-ля doc_type, например.

    "В случае с хранением данных принято использовать реляционные СУБД"? Не существует Единственно Правильного Подхода.

    TC, Redis и прочие key-value хранилища нужны только лишь для, например, хранения сессий, организации pubsub. Не стоит такие вещи использовать как полноценные хранилища и, соответственно, так же не стоят того, чтобы задумываться о решении "как бы проблемы" объектного подхода.

    Вы, вообще, интересовались возможностями TC? Рекомендую посмотреть хотя бы презентацию, что на сайте у Микио. TC TDB -- самая настоящая DODB.

    Теперь о CouchDB, Riak - тут вопрос совсем другой и данные, в результате отработки MapReduce, могут быть совершенно разные и решения того как можно вогнать абсолютно, так сказать, рандомные данные в модель, лично я не вижу.

    CouchDB поощряет "изнаночный" подход, который трудно ложится на модели. Честно говоря, не вижу смысла делать бэкенд для этой базы, потому что получится прогибание инструмента под цели, для которых он не предназначен.

    Если подходить к NoSQL с точки зрения моделей, то это не так уж и нужно, ибо они используются для достаточно сложных задач, где даже "самый замечательный орм в мире" просто будет либо адски сложным, либо он тупо не будет существовать вообще.

    Не понял смысла абзаца. Модели нужны для сложных задач? Нереляционные БД нужны для сложных задач? Модели не нужны? На каких задачах какие сложности должны возникнуть и почему их не возникает в каких-то других ситуациях?

  15. Serge Koval

    Вот я думал над концепцией модель=запрос, и пока не представляю как это будет работать.

    Понятно как оно будет работать, когда запрашиваем данные. Но как оно будет работать, когда мы сохраняем данные?

    Просто я воспринимаю модель как некую абстракцию, которая описывает некую структуру данных. Одна структура = одна модель.

    В случае, если воспринимать модель как вариант ответа при запросе, подразумевается что в базе данных данные могут пересекаться для таких моделей и наступит хаос.

    Чего я не понимаю? :-)

  16. Andy Mikhailenko reporter

    (сорри, сонный мозг генерирует раз в 10 больше текста, чем надо)

    Опишу юзкейс для PyModels (пока оно не совсем так работает, но близко):

    • User(username, password, first_name, last_name)
    • Employee(first_name, last_name, department, position)
    u = User(username='john', password='qwerty', first_name='John')
    u.save()    # сохраняются username, password, first_name, last_name
    e = u.convert_to(Employee)    # словарик данных тот же, валидаторы другие
    e.last_name = 'Doe'    # u.last_name != e.last_name
    e.department = 'Rocket Science dept.'
    e.position = 'Chair'
    e.save()    # или e.save(granular=True)
    

    Зачем?

    Зачем вообще такое надо? Сначала посмотрим, что нам дает вышеприведенный пример:

    >>> u = User.objects(db).where(username='john')[0]
    >>> u.last_name
    "Doe"
    >>> u.organization
    Traceback ...
    ...
    AttributeError ...
    >>> u._state.data['organization']
    "Rocket Science dept."
    >>> e = u.convert_to(Employee)
    >>> e.organization
    "Rocket Science dept."
    

    Каждая модель показывает только те данные, с которыми знает, что делать. При этом она знает и об остальных данных. Любое преобразование происходит явным образом. При сохранении проводится валидация документа в выбранной плоскости.

    Это может быть полезно в workflow. Например, вот фрагменты GTD: stuff.convert_to(ToDo), stuff.convert_to(Project), stuff.convert_to(SomedayMaybe), to_do.convert_to(DelegatedAction), delegated_action.convert_to(CompletedAction) и так далее. Я потихоньку делаю органайзер, в котором благодаря такому подходу отсутствует часть традиционного клеевого кода, и пока не напоролся на проблемы.

    Прошу заметить, мы таким образом не пихаем логику из вьюх в модели, а выносим клей в стороннюю библиотеку и наполняем модели немного иным смыслом. Без "convert_to" пришлось бы брать две модели, ссылаться из одной на другую, проставлять в одной из них статус, сохранять обе; при следующем шаге -- то же самое, но другими словами. С "convert_to" волшебства нет: просто описания сущностей накладываются друг на друга. Может быть, старая форма продолжит соответствовать старой модели. Может быть, нет. В реляционной БД мы удалили бы запись из одной таблицы и скопировали бы ее куски в другую, но обе записи описывали бы один и тот же объект реального мира. Задумка перешла из фазы "идея" в фазу "проект", у нее появились новые атрибуты, исчезли некоторые прежние, но это всё та же задумка. Дом покрасили в другой цвет и пристроили флигель, но дом всё тот же. Село получило статус города, но оно никуда не переехало. Записка то передается кому-то, то возвращается с комментарием, но она та же. Вместе с тем, мы не мыслим категориями "выбрать из проектов те, где владелец -- я" или "выбрать из задач те, где дата завершения больше или равна текущей дате плюс пять дней". Мы говорим: "мой проект", "актуальная задача". И мы привыкли городить вьюшки под каждый концепт. А ведь можно просто брать да называть вещи сразу своими именами:

    class UpcomingTask(Task):
        class Meta:
            must_have = {'deadline__lte': datetime.date.today() + datetime.timedelta(days=5)}
    
    # ога, прямо тут же, ибо все модели -- ad hoc
    print UpcomingTask.objects(db).count()
    

    Как?

    Теперь посмотрим еще раз на первоначальный пример.

    1. при obj.save() документ сохраняется в том виде, в каком он был при создании питоньего объекта (в нашем случае это все поля модели User) + новые данные, если они относятся к данной модели (Employee).
    2. при obj.save(granular=True) сохраняется документ в том виде, в котором он на момент сохранения был в БД + новые данные, если они относятся к данной модели (Employee).

    Первый вариант, по-моему, совершенно нормален, если только не возникает конкуренции моделей за 1 объект. Но это мало чем отличается от конкуренции питоньих объектов, представляющих одну запись в жесткой таблице.

    Второй вариант устраняет конкуренцию, но может породить неадекватные комбинации полей в одном документе. С другой стороны, то же самое можно было бы сказать и об UPDATE x SET a=b, где "a" -- не единственное поле. Еще второй вариант может добавлять оверхед на чтение перед записью; чаще всего это можно обойти server-side (напр. расширением на Lua для Tokyo Tyrant -- несколько строк).

    Выражение bar = foo.convert_to(Bar) выглядит очень необычно, потому что мы к такому не привыкли. Кроме того, оно может быть вредным, если мы захотим работать дальше с обоими объектами (foo и bar), особенно на запись и без granular=True. Но, опять же, от программиста ожидается применение мозга.

    Disclaimer: не знаю, насколько это применимо к сабжу, просто озвучил мысль подробнее. Мопед не мой, он сам пришел.

  17. Serge Koval

    Примерно начал представлять.

    Такой подход, наверное, имеет смысл для nosql баз данных - оно не впишется в традиционные реляционные модели. Например тем, что схема не определяется моделью.

    Вопрос по первому примеру: User и Employee в базе хранятся вместе, просто поля разные, или они логически разделены? Т.е. не будет ли проблем, когда я сохраню Employee, потом прочитаю его как юзера и у него не будет пароля?

    stuff.convert_to можно сделать и для простых моделей, это просто копирование словариков/аттрибутов. Хотя я бы делал через наследование, тогда понятно какие отношения между моделями. Например в алхимии наследования аж три штуки: 1. Как расширение существующей схемы (модель Б наследует А, таблица А содержит поля и для А и для Б) 2. Как еще одна таблица и join между ними 3. Как две отдельные таблицы, join'а нет

  18. Andy Mikhailenko reporter

    Ну если позанудствовать, то схема и так моделью не определяется, скорее модель привязывается к схеме более или менее плотно в зависимости от паттерна.

    Собсно, хочется не навязывать ничего ни реляционным, ни документоориентированным. Потому и полезно связывание как-то возложить на бэкенд вплоть до выбора стратегии.

    По тому примеру: это именно один документ. Если проще (с API PyModels+Pyrant):

    from pymodels import *
    
    class A(Model):
        x = Property()
    
    class B(Model):
        y = Property
    
    >>> db = get_storage(backend='pymodels.backends.tokyo_tyrant')
    >>> a = A(x='foo')
    >>> a.save(db)
    >>> b = a.convert_to(B)
    >>> b.y = 'bar'
    >>> b.save()
    >>> db.connection[a.pk]    # напрямую смотрим в хранилище через Pyrant
    {'x': 'foo', 'y': 'bar'}
    

    Проблема будет только если после этого выполнить a.save(), потому что a пока не знает о новых атрибутах. Зато a.save(granular=True) даже в этой ситуации ничего не сотрет.

    Не очень понимаю, как для реляционных моделей может работать stuff.convert_to, ведь словарик на схему может и не лечь. Тут еще вот какой момент: если речь о workflow (объект один, форма/суть плавно меняются), простое наложение моделей может дать широкий спектр преобразований, в том числе документ может исчезнуть из прежней модели-запроса и появиться в разных других.

    (Когда мы играем музыку, мы вряд ли станем сильно задумываться о том, к каким жанрам кто-то потом ее отнесет; нам просто нравится это, вот и всё. И здесь тоже: какая это "модель поведения" -- не знаем и не важно это. Когда надо будет -- приложим модель к документу и узнаем, (уже) подходит или (уже) нет.)

    Ну а РСУБД принуждает к обратной последовательности: сначала определить модели, затем заняться перекладыванием данных туда-сюда. И "convert_to" придется либо писать самостоятельно каждый раз (как это и делается обычно), либо обильно оснащать метаданными. Может, я не улавливаю чего-то?

    Про типы наследования не знал, интересно. А как работает третий способ наследования и зачем он?

    Опять же, в PyModels наследование есть, но оно используется для совсем другой цели: легкое расширение запросов. То есть, CompletedTask(Task) -- это такая Task, у которой проставлена дата завершения, а список атрибутов и идентифицирующий запрос -- те же:

    class Task(Model):
        text = Property(required=True)
        class Meta:
            must_have = {'actionable': True, 'text__exists': True}
    
    class CompletedTask(Task):
        completed = DateTime(required=True)
        class Meta:
            # (тут тавтология, надо бы угадывать все exists/not-empty из определений свойств)
            must_have = {'completed__exists': True, 'completed__not': ''}
    
    >>> ct = CompletedTask(text="поспать")
    >>> ct.save(db)
    >>> CompletedTask.objects(db)
    [<CompletedTask: поспать>]
    >>> Task.objects(db)
    [<Task: пободрствовать>, <Task: поспать>]
    

    То есть, в pymodels наследование подразумевает, что модель-наследник описывает часть объектов родителя, а родитель описывает все объекты любого наследника (если тот дополняет, а не переопределяет правила). Значит, можно спокойно называть вещи своими именами, коих у каждой вещи в реальном мире много. Можно ли такое поведение адекватно изобразить в Алхимии и надо ли?

    Модели User и Employee тоже можно определить наследованием. Препятствий два, оба не технические: 1) что первично -- курица или яйцо, пользователь или сотрудник? Пожалуй, нет смысла считать одну модель более общей; 2) делать третью модель -- "Person" -- можно, но нам она просто-напросто не нужна в гипотетической системе. Конечно, если для нас будет важно обеспечить ряд общих полей для User и Employee, нужно будет определить такие поля в Person. Страх alter table надо гнобить, т.к. при бессхемности для сцепления двух моделей вполне достаточно тривиального скриптика.

    Действительно серьезной проблемой при переходе на такой способ может быть "жадность" моделей (модель с пустым must_have соответствует абсолютно всем документам хранилища). Можно просто не догадаться, что какой-то объект "без спросу" соответствует нашей модели. Поэтому защитной методикой может быть указание в идентифицирующем запросе не только содержательных полей, но и вопросов разной точности: "is_actionable", "is_task"и тд. Последний не очень далек от "type=task", но чрезвычайно важно, что он не ограничивает кол-во "типов" одного документа, тогда как "type=x" заставляет опять пихать записи в прокрустово ложе таблиц.

  19. Anonymous

    Позволю себе влезть в обсуждение. Не могу согласиться с парой предложений.

    В самом деле, если модель соответствует жестко указанному пространству имен и описывает (в любом случае) жестко заданный набор полей, зачем отказываться от RDBMS? А вот если модель описывает некоторое подмножество разнородных документов, хранящихся в одном пространстве имен, то она более чем применима к такому хранилищу.

    Почему же. По-моему модель должна жестко описывать структуру данных даже в случае схема-лесс СУБД. Ибо если она будет отсутствовать, то мне не совсем понятно как генерировать тогда те же формы к модели. А преимущество таких БД, как мне кажется, проявляется в случае наследования моделей. Дальше буду говорить на примере mongoDB, только с ней немного знаком из NoSql БД.

    User(Document): username = models.StringColumn() ...

    class Firm (User): address = models.StringColumn()

    Модели имеют уникальные поля, однако, храниться объекты обоих типов будут в пределах одной коллекции. Пусть это будет db.users. Соответственно при выборке db.users.find() мы получим всех пользователей. Хотя тут не обойтись без дополнительного аттрибута "_type", ссылающегося на описание модели.

    Andy, очень много текста, трудно читать и следить за ходом Ваших мыслей. Прошу прощения, если понял неверно.

  20. Andy Mikhailenko reporter

    По-моему модель должна жестко описывать структуру данных даже в случае схема-лесс СУБД. Ибо если она будет отсутствовать, то мне не совсем понятно как генерировать тогда те же формы к модели.

    Так и есть. Просто модель != документ; модель != коллекция. Ваш пример это тоже иллюстрирует. Или я чего-то не понял?

    class Firm (User)

    Это как?

    Хотя тут не обойтись без дополнительного аттрибута "_type", ссылающегося на описание модели.

    Как насчет DRY + loose coupling + separation of concerns? Реальные прототипы записей в БД (т.е. объекты реального мира) не содержат названий; названия хранятся в голове воспринимающего. Так и здесь: база хранит Просто Данные, а программа их "мысленно" организует в некие модели, иерархии, семантические сети, расклеивает ярлычки. Модель = метаданные. База данных "глупа".

    Естественно, это не аксиома, а просто возможный подход, который представляется интересным.

    Andy, очень много текста, трудно читать и следить за ходом Ваших мыслей

    Вы правы, сорри. Пока нет времени, чтобы урезать поток до четких тезисов. Одну из попыток см. в readme к pymodels (в тч на pypi).

  21. Anonymous

    Насчет примеров с User и Firm действительно не самый удачный пример. Имелось ввиду, что в качестве пользователей могут выступать физ.лица и юр.лица :) Это не FK связи там) Просто наследуем модель пользователя с добавлением уникальных полей (address).

    Так и есть. Просто модель != документ; модель != коллекция. Ваш пример это тоже иллюстрирует. Или я чего-то не понял?

    Вот тут я ничего не понял. Ну да, модель.. — схема, описание. Аналогично понятиям класс-объект.. модель - запись в бд.

    Реальные прототипы записей в БД (т.е. объекты реального мира) не содержат названий; названия хранятся в голове воспринимающего. Так и здесь: база хранит Просто Данные, а программа их "мысленно" организует в некие модели, иерархии, семантические сети, расклеивает ярлычки. Модель = метаданные. База данных "глупа".

    Безусловно, это все хорошо и логично. Но вопрос как генерировать meta информацию для тех же форм? Откажемся от описания "схемы" в моделях, будем вынуждены сделать это в тех же формах. Да и держать в памяти это трудно. Гораздо проще, когда в модели наглядно представлены возможные поля. А преимущество схема-лесс БД, как уже говорил, проявится в возможности наследования моделей и хранения однотипных объектов в пределах коллекции.

    Ну а что касается служебного поля "_type".. Да, тоже не в восторге. Но это самое простое решение. Но это самое простое и быстрореализуемое решение из тех, что приходят в голову. Кстати аналогично тип хранится в mongoengine (backend для django) если не ошибаюсь.

    Ну и + такой вариант без проблем уживется с реляционными базами.

    Если я неверно понял вышеизложенные мысли прошу поправить меня.

  22. Anonymous

    PS Упустил при ответе Вашу фразу "Модель = метаданные.". Как вы себе это представляете? Интересует именно реализация

  23. Anonymous

    PPS Глянул pymodel, я правильно понимаю, что для того, чтобы ассоиировать запись в БД с моделью используется значение must_have из Meta?

  24. Andy Mikhailenko reporter

    Если я неверно понял вышеизложенные мысли прошу поправить меня.

    По-моему, неверно. Значит, я не смог донести мысль. Попробую снова.

    Есть объект. У него может быть произвольный набор свойств. Нам никогда не важны они все сразу, но нельзя заранее знать, какой именно набор потребуется для такой-то цели.

    Концепт вместо иерархии. Иногда нам нужны "все красные", иногда "все фрукты", иногда "все яблоки", иногда "все растения", иногда "всё мягкое", иногда "всё съедобное", иногда "всё, что в корзине", иногда "всё, что помещается в карман". Иерархия не поможет. Помогут концепты, образы, т.е. некие наборы слотов. Что такое "яблоко"? Что такое "объект карманных размеров"? Описали концепт, приложили его к окружающему миру, получили набор совпадений. И наоборот: взяли холодильник, назвали его яблоком -- мозг сопротивляется, потому что внутренняя валидация не проходит.

    Метаданные отделены от данных. Модель -- это жесткое описание, и форму из модели можно сгенерировать, и валидация работает, но она существует сама по себе ("в голове"), а яблоко -- само по себе ("в мире"). И не надо на нем вырезать ножом слово "яблоко", оно яблочнее от этого не станет. И пользователь не станет пользовательнее, если ему на лбу написать "_type=user" или запихать его в специальную Комнату Пользователей (таблицу/коллекцию/...) и запретить оттуда вылезать.

    Другими словами, предлагается заранее не сортировать и заранее не надписывать, но выделять значимые свойства.

    Но вопрос как генерировать meta информацию для тех же форм? Откажемся от описания "схемы" в моделях, будем вынуждены сделать это в тех же формах. Да и держать в памяти это трудно. Гораздо проще, когда в модели наглядно представлены возможные поля.

    Так и есть. По-моему, мы с Вами описываем одно и то же, просто я дополнительно предлагаю вынести типизацию документов полностью в приложение. Этакий duck typing, если хотите. "foo = 1" -- ясно же, что это число, а не строка или список. Вот и с записями в БД так.

    PS Упустил при ответе Вашу фразу "Модель = метаданные.".

    Как вы себе это представляете? Интересует именно реализация

    Посмотрите PyModels. Там есть подробные примеры. Исходники открыты. На конкретные вопросы отвечу. Реализация не оптимальная, но уже больше, чем эксперимент.

  25. Andy Mikhailenko reporter

    PPS Глянул pymodel, я правильно понимаю, что для того, чтобы ассоиировать запись в БД с моделью используется значение must_have из Meta?

    Да, must_have и must_not_have. В идеале там должен быть обычный запрос + вывод ограничений из описания самих свойств (атрибут required и тд). Я пока не стал с этим возиться, потому что нет времени, ну и пока отрабатываю саму концепцию на разных юзкейсах (органайзер, журнал и тд).

  26. Serge Koval

    Иногда нам нужны "все красные", иногда "все фрукты", иногда "все яблоки", иногда "все растения", иногда "всё мягкое", иногда "всё съедобное", иногда "всё, что в корзине", иногда "всё, что помещается в карман".

    Ну тогда это не свойство модели - это процесс выбора модели по критерию, а модель описывает объект с кучей свойств: цвет, тип, фенотип, положение и т.д.

    Если объект описывается несколькими моделями, получается что:

    1. На каждый кусочек описания нужно делать отдельный объект

    2. Что бы узнать свойства всего объекта, придется получить некое множество из всех моделей, которые с ним работают

    3. Усложняется работа программиста, что может привести к забавным багам. Скажем, яблоко, красное яблоко и красное яблоко на дереве потребуют три разных модели, хотя по сути работают с одним объектом: тип+цвет+положение.

    Я понимаю зачем такое может понадобиться - денормализация данных. Но я не могу как она может заменить реляции - отношения между объектами на одной денормализации не построишь. В примере выше, когда есть явная зависимость между объектами (User, Employee) - не проблема, тут денормализация только поможет. Но они и храниться могут отдельно, а при обращении к Employee мы все равно получим доступ к полям User.

    Возможно я не понял, поправь если что :-)

    Кстати, в алхимии можно работать на уровне полей, а не на уровне моделей. Т.е. такой вот запрос вполне возможен:

    items = User.objects.query(User.name, User.email).all()
    for i in items:
        print i.name, i.email
    

    (если я синтаксис правильно помню, возможно там i['name'], а то и i[0], надо проверить).

    К чему я это все:

    1. Разные сущности лучше хранить отдельно

    2. Если есть похожие сущности, лучше определить их иерархию, например через наследование, как наиболее привычный способ.

    Это пока первые мысли, я еще пару раз предыдущие посты перечитаю, возможно я что-то упустил :-)

  27. Andy Mikhailenko reporter

    (не знаю, почему не ответил 5 мес назад, попробую сейчас))

    Если объект описывается несколькими моделями, получается что:

    1. На каждый кусочек описания нужно делать отдельный объект

    Отдельный объект (экземпляр типа документа) может потребоваться для конкретной цели. Если у нас есть цель поработать с кусочком описания — будут отдельные объекты. Если есть цель поработать с полным документом — будет один объект. Разве не так?

    2. Чтобы узнать свойства всего объекта, придется получить некое множество из всех моделей, которые с ним работают

    Зачем? Можно простым алгоритмом подобрать наиболее полную схему из известных. Или вообще не думать об этом. Или посмотреть в instance._saved_state.data.

    3. Усложняется работа программиста, что может привести к забавным багам. Скажем, яблоко, красное яблоко и красное яблоко на дереве потребуют три разных модели, хотя по сути работают с одним объектом: тип+цвет+положение.

    Так в том и дело, что эта проблема возникает с реляционными БД, а не с документоориентированными.

    Кстати, в Docu (бывш. PyModels) базовый класс для всех документосхем — Document — вовсе лишен схемы и работает как простой словарик с persistence.

    Я понимаю зачем такое может понадобиться - денормализация данных. Но я не могу как она может заменить реляции - отношения между объектами на одной денормализации не построишь. В примере выше, когда есть явная зависимость между объектами (User, Employee) - не проблема, тут денормализация только поможет. Но они и храниться могут отдельно, а при обращении к Employee мы все равно получим доступ к полям User.

    Полностью избавиться от ссылок между документами невозможно, да и не нужно. Я у себя в органайзере использую и тот и другой подход. Юзкейс User/Employee демонстрирует, на мой взгляд, не зависимость между сущностями, а их единство. Речь об одном объекте реального мира, просто нам в разных случаях важны разные его свойства, отчасти пересекающиеся. Не надо лишний раз прогибать данные под логику работы с ними. Впрочем, it depends.

    1. Разные сущности лучше хранить отдельно

    Конечно! :)

    2. Если есть похожие сущности, лучше определить их иерархию, например через наследование, как наиболее привычный способ.

    Привычный != лучший. Но в ряде случаев это работает.

  28. Log in to comment