SQLAlchemy: картограф изменяет мои объекты? - PullRequest
9 голосов
/ 02 марта 2012

Я пытаюсь использовать SQLAlchemy версию для хранения моих объектов в базе данных. У меня есть save(...) функция для этой цели:

#!/usr/bin/env python
# encoding: utf-8

from sqlalchemy     import Column, Integer, MetaData, String, Table, create_engine
from sqlalchemy.orm import mapper, sessionmaker

class MyClass(object):
    def __init__(self, title):
        self.title = title
    def __str__(self):
        return '%s' % (self.title)

def save(object_list):
    metadata = MetaData()
    my_class_table = Table('my_class',
                            metadata,
                            Column('id',    Integer,     primary_key=True),
                            Column('title', String(255), nullable=False))

    # everything is OK here, output:
    # some title
    # another title
    # yet another title

    for obj in object_list:
        print obj

    mapper(MyClass, my_class_table)

    # on Linux / SQLAlchemy 0.6.8, this fails with
    # Traceback (most recent call last):
    #   File "./test.py", line 64, in <module>
    #     save(my_objects)
    #   File "./test.py", line 57, in save
    #     print obj
    #   File "./test.py", line 11, in __str__
    #     return '%s' % (self.title)
    #   File "/usr/lib/python2.7/dist-packages/sqlalchemy/orm/attributes.py", line 167, in __get__
    #     return self.impl.get(instance_state(instance),
    # AttributeError: 'NoneType' object has no attribute 'get'

    # on Mac OSX / SQLAlchemy 0.7.5, this fails with
    # Traceback (most recent call last):
    #   File "./test.py", line 64, in <module>
    #     save(my_objects)
    #   File "./test.py", line 57, in save
    #     print obj
    #   File "./test.py", line 11, in __str__
    #     return '%s' % (self.title)
    #   File "/Library/Python/2.7/site-packages/sqlalchemy/orm/attributes.py", line 165, in __get__
    #     if self._supports_population and self.key in dict_:
    #   File "/Library/Python/2.7/site-packages/sqlalchemy/orm/attributes.py", line 139, in __getattr__
    #     key)
    # AttributeError: Neither 'InstrumentedAttribute' object nor 'Comparator' object has an attribute '_supports_population'

    for obj in object_list:
        print obj

    # (more code to set up engine and session...)


if __name__ == '__main__':
    my_objects = [MyClass('some title'), MyClass('another title'), MyClass('yet another title')]
    save(my_objects)

Мне кажется, что картограф делает что-то с моими объектами неявно, когда я создаю картограф, и я больше не могу их использовать. Из того, что я прочитал в похожих вопросах , это не совсем неизвестно.

Ожидается ли такое поведение?
Я что-то не так понимаю здесь?
Как правильно отображать и хранить мои объекты?


Дополнительная информация: Я использую SQLAlchemy 0.7.5 с системным Python 2.7.1 по умолчанию на Mac OSX 10.7.3 Lion и SQLAlchemy 0.6.8 с системным Python 2.7.2+ по умолчанию на виртуальной машине Kubuntu 11.10.


Обновление: Похоже, что маппер SQLAlchemy хорошо известен для изменения объектов на «потребности SQLAlchemy». Решение в связанной статье заключается в создании объектов после вызова mapper(...).
У меня уже есть допустимые объекты - но я больше не могу их использовать ...

Как мне получить SQLAlchemy для хранения моих объектов?

Обновление 2: У меня складывается впечатление, что я неправильно понимаю что-то фундаментальное в ОРМ:
Я думал, что концепция сопоставления SQLAlchemy дает мне возможность определять свои объекты и работать с ними в моем приложении, отделенном от любой базы данных - и только когда я хочу сохранить их, я ввожу SQLAlchemy и заставляю его выполнять всю тяжелую работу по отображению Объект класса для таблицы базы данных. Кроме того, я не вижу архитектурной причины, по которой SQLAlchemy следует упоминать где-либо еще в моем коде, кроме функций сохранения save(...) и load(...).

Однако, глядя на первый ответ ниже, я понимаю, что должен сопоставить класс с таблицей базы данных, прежде чем использовать какие-либо объекты; это было бы в самом начале моей программы. Может быть, я что-то здесь не так, но это кажется довольно резким ограничением дизайна со стороны SQLAlchemy - все слабые связи исчезли. Имея это в виду, я также не вижу преимуществ в том, чтобы иметь в первую очередь маппер, поскольку «поздняя связь», которую я хочу, технически невозможна с SQLAlchemy, и я мог бы просто сделать этот «декларативный стиль» и смешать и смешайте бизнес-логику с кодом постоянства: - (

Обновление 3: Я вернулся к исходной точке и снова перечитал SQLAlchemy. Это написано прямо на главной странице :

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

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

Что я делаю не так?

Обновление 4:

Для полноты картины я хотел бы добавить рабочий пример кода, который выполняет все сопоставления перед созданием каких-либо бизнес-объектов:

#!/usr/bin/env python
# encoding: utf-8

from sqlalchemy     import Column, Integer, MetaData, String, Table, create_engine
from sqlalchemy.orm import mapper, sessionmaker

class MyClass(object):
    def __init__(self, title):
        self.title = title
    def __str__(self):
        return '%s' % (self.title)

def save(object_list, metadata):

    engine  = create_engine('sqlite:///:memory:')
    metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    session = Session()

    for obj in object_list:
        try:
            session.add(obj)
            session.commit()
        except Exception as e:
            print e
            session.rollback()

    # query objects to test they're in there
    all_objs = session.query(MyClass).all()
    for obj in all_objs:
        print 'object id   :', obj.id
        print 'object title:', obj.title


if __name__ == '__main__':

    metadata = MetaData()
    my_class_table = Table('my_class',
                            metadata,
                            Column('id',    Integer,     primary_key=True),
                            Column('title', String(255), nullable=False))
    mapper(MyClass, my_class_table)

    my_objects = [MyClass('some title'), MyClass('another title'), MyClass('yet another title')]
    save(my_objects, metadata)

В своем собственном (не примерном) коде я импортирую MyClass из его собственного модуля и выполняю сопоставление в отдельном классе «хранилище объектов», который создает MetaData в своем методе __init__(...) и сохраняет его в член, поэтому методы постоянства save(...) и load(...) могут обращаться к нему по мере необходимости. Все, что требуется от основного приложения, - это создать объект репозитория в самом начале; он содержит влияние SQLAlchemy на проектирование для одного класса, а также распределяет определения бизнес-объектов и сопоставленных таблиц по разным местам в коде. Еще не уверен, пойду ли я с таким долгим сроком, но на данный момент это работает.

Последний быстрый совет для новичков SQLAlchemy, таких как я: вы должны работать с одним и тем же объектом метаданных повсюду, в противном случае вы получите исключения, такие как no such table или class not mapped.

Ответы [ 2 ]

2 голосов
/ 03 марта 2012

SQLAlchemy абсолютно изменяет сопоставленный класс. SQLAlchemy называет это инструментарием .

Наблюдать:

>>> print(MyClass.__dict__)
{'__module__': '__main__', '__str__': <function __str__ at 0x106d50398>, '__dict__':
<attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 
'MyClass' objects>, '__doc__': None, '__init__': <function __init__ at 0x106d4b848>}

>>> mapper(MyClass, my_class_table)
Mapper|MyClass|my_class
>>> print(MyClass.__dict__)
{'__module__': '__main__', '_sa_class_manager': <ClassManager of <class 
'__main__.MyClass'> at 7febea6a7f40>, 'title': 
<sqlalchemy.orm.attributes.InstrumentedAttribute object at 0x10fba4f90>, '__str__': 
<function __str__ at 0x10fbab398>, 'id': <sqlalchemy.orm.attributes.InstrumentedAttribute 
object at 0x10fba4e90>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, 
'__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None, '__init__':
 <function __init__ at 0x10fbab410>}

Аналогично, существует разница между обычными MyClass экземплярами и инструментальными MyClass экземплярами:

>>> prem = MyClass('premapper')
>>> mapper(MyClass, my_class_table)
Mapper|MyClass|my_class
>>> postm = MyClass('postmapper')
>>> print prem
{'title': 'premapper'}
>>> print postm
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x10e5b6d50>, 'title': 'postmapper'}

Ваша ошибка NoneType заключается в том, что теперь инструментированный MyClass.title (который был заменен дескриптором InstrumentedAttribute) пытается получить свойство _sa_instance_state через instance_state(instance). _sa_instance_state создается инструментированным MyClass при создании объекта, но не необученным MyClass.

Инструментарий необходим. Это сделано для того, чтобы доступ к атрибутам, назначение и другие важные изменения состояния объекта могли быть переданы в маппер с использованием дескрипторов. (Например, как может быть возможна отложенная загрузка столбцов или даже доступ к коллекции без изменения класса для контроля доступа к атрибутам в объекте?) MyClass объекты не привязаны к таблице , , но они должны быть привязан к мапперу . Вы можете изменить параметры отображения и таблицу, не изменяя код ваших доменных объектов, но не обязательно сами ваши доменные объекты . (В конце концов, вы можете подключить один объект к нескольким картографам и нескольким таблицам.)

Думайте об инструментарии как о прозрачном добавлении реализации «Субъекта» к отображаемому классу для преобразователя «Наблюдатель» в шаблоне субъект-наблюдатель. Очевидно, что вы не можете реализовать шаблон субъект-наблюдатель на произвольном объекте - вам нужно согласовать наблюдаемый объект с интерфейсом субъекта.

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

Единственный способ, которым вы даже можете избежать использования полностью неинструментированных объектов приложения, - это если ваш сценарий использования является чрезвычайно тривиальным, что-то вроде эквивалента pickle или unpickle диктов. Например, без инструментов вы никогда не сможете загрузить или сохранить связанные объекты в атрибутах коллекции. Вы действительно никогда не собираетесь делать это? Если это так, возможно, вы будете более счастливы, если вообще не используете sqlalchemy.orm и просто используете базовый Expression API для сохранения ваших экземпляров?

0 голосов
/ 02 марта 2012

Вы должны выполнить сопоставление до создания объектов MyClass, либо в начале 'if', либо перед этим.

Есть ли конкретная причина, по которой вам нужно создавать экземпляры, прежде чем даже решить, к какой таблице они относятся?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...