I have following method in my framework:

    @models.managed_commit
    def create_article(self, user_uid, title, **kwargs):
        user = user_manager.UserManager().get_user_by_uid(user_uid)

        # create empty article
        article = models.Article(user=user, title=title)

        # if profiles were supplied, update the model
        self._process_profiles(kwargs)

        self._attach_new_tags(kwargs.pop('tags', []), article)

        # we can now simply update
        article.update_from_dict(kwargs)

        if not article.slug:
            article.slug = utils.strings.slugify(title)

        models.Session.add(article)

        return article

    def _attach_new_tags(self, tags, article):
        if not tags:
            return

        tman = tag_manager.TagManager()
        for tag in tags:
            try:
                models.Session.query(models.ArticleTag).filter(
                    (models.ArticleTag.article_uid == article.uid) &
                    (models.ArticleTag.tag_uid == tag.uid)).one()
            except sqlalchemy.orm.exc.NoResultFound:
                tag = tman.get_tag_by_uid(tag.uid)

                obj = models.ArticleTag(tag=tag)

                with models.Session.no_autoflush:
                    article.article_tags.append(obj)

    @no_autoflush
    def _process_profiles(self, kwargs):
        if 'profiles' not in kwargs:
            return

        profiles = []

        # rewrite profile if any
        if kwargs['profiles']:
            for profile in kwargs['profiles']:
                profiles.append(self._get_profile(profile.uid))

        kwargs['profiles'] = profiles

    Models:

    class Article(base.Base, mixin.PublishableMixin):
        user_uid = sqlalchemy.Column(
            sqlalchemy.Unicode(32), sqlalchemy.ForeignKey('user.uid'), 
index=True)
        title = sqlalchemy.Column(sqlalchemy.Unicode(255), nullable=False)
        meta_description = sqlalchemy.Column(sqlalchemy.Unicode(255))
        meta_title = sqlalchemy.Column(sqlalchemy.Unicode(255))

    class PublishableMixin(base_mixin.BaseMixin):
        '''Adds publishable columns to a model'''
        published_at = sqlalchemy.Column(
            sqlalchemy.DateTime(timezone=True), default=None, index=True)
        published_at._creation_order = 9996  # pylint: 
disable=protected-access

    class BaseModel(object):
        def update_from_dict(self, data):
            _props = [
                prop for prop in sqlalchemy.orm.object_mapper(
                    self).iterate_properties]
            props = [prop.key for prop in _props if isinstance(
                prop, sqlalchemy.orm.ColumnProperty)]

            for key in data.keys():
                if key in props:
                    try:
                        setattr(self, key, data.pop(key))
                    except AttributeError:
                        raise AttributeError('Cant set attribute: %s' % key)
                elif hasattr(self, key):
                    try:
                        setattr(self, key, data.pop(key))
                    except AttributeError:
                        raise AttributeError('Cant set attribute: %s' % key)
                else:
                    raise AttributeError(
                        'Key ({}) is not mapped property of {}'.format(
                            key, self.__class__.__name__))


This code in scenario when user creates article with tags that are m2m 
objects will cause very strange exception KeyError 'published_at', where 
published_at is a column on Article model that should be set to null in my 
case (as default value):

    [2015-12-14 13:55:14] 'published_at'
    Traceback (most recent call last):
      File "/home/user/project/proj_x/server.py", line 44, in _call_view
        response = view(**data)
      File "/home/user/project/proj_x/decorators.py", line 58, in inner
        return fun(*args, **kwargs)
      File "/home/user/project/proj_x/decorators.py", line 69, in inner
        return fun(*args, **kwargs)
      File "/home/user/project/proj_x/decorators.py", line 137, in 
inner_inner
        return f(*args, **kwargs)
      File "/home/user/project/proj_x/versions/v0_0/article_views.py", line 
548, in create_article
        resp = cndcore.iapi.article.create_article(user=request.user, 
**form_data)
      File "/home/user/Projects/cndcore/cndcore/iapi/article.py", line 223, 
in create_article
        return obj.to_response()
      File 
"/home/user/Projects/cndcore/cndcore/models/mixin_exportable.py", line 298, 
in to_response
        setattr(obj, name, self.get_value(expand, name, cols))
      File 
"/home/user/Projects/cndcore/cndcore/models/mixin_exportable.py", line 335, 
in get_value
        getattr(self, name), expand=expand)
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/attributes.py",
 
line 237, in __get__
        return self.impl.get(instance_state(instance), dict_)
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/attributes.py",
 
line 583, in get
        value = self.callable_(state, passive)
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/strategies.py",
 
line 532, in _load_for_state
        return self._emit_lazyload(session, state, ident_key, passive)
      File "<string>", line 1, in <lambda>
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/strategies.py",
 
line 602, in _emit_lazyload
        result = q.all()
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/query.py",
 
line 2423, in all
        return list(self)
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/query.py",
 
line 2570, in __iter__
        self.session._autoflush()
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/session.py",
 
line 1293, in _autoflush
        self.flush()
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/session.py",
 
line 2015, in flush
        self._flush(objects)
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/session.py",
 
line 2133, in _flush
        transaction.rollback(_capture_exception=True)
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/util/langhelpers.py",
 
line 60, in __exit__
        compat.reraise(exc_type, exc_value, exc_tb)
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/session.py",
 
line 2097, in _flush
        flush_context.execute()
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/unitofwork.py",
 
line 373, in execute
        rec.execute(self)
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/unitofwork.py",
 
line 532, in execute
        uow
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/persistence.py",
 
line 170, in save_obj
        mapper, table, update)
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/persistence.py",
 
line 635, in _emit_update_statements
        lambda rec: (
      File 
"/home/user/Envs/api/local/lib/python2.7/site-packages/sqlalchemy/orm/persistence.py",
 
line 454, in _collect_update_commands
        value = state_dict[propkey]
    KeyError: 'published_at'

I found two different solutions to this problem.

First one is to wrap more code with no_autoflush:

    def _attach_new_tags(self, tags, article):
        if not tags:
            return

        tman = tag_manager.TagManager()
        with models.Session.no_autoflush:
            for tag in tags:
                try:
                    models.Session.query(models.ArticleTag).filter(
                        (models.ArticleTag.article_uid == article.uid) &
                        (models.ArticleTag.tag_uid == tag.uid)).one()
                except sqlalchemy.orm.exc.NoResultFound:
                    tag = tman.get_tag_by_uid(tag.uid)

                    obj = models.ArticleTag(tag=tag)

                    article.article_tags.append(obj)

However what bothers me in it, next time something similar will happen, 
sqlalchemy will throw exception that will be again obfuscating real problem 
(Query-invoked autoflush).

Second solution is small patch for sqlalchemy itself:

    diff --git a/lib/sqlalchemy/orm/persistence.py 
b/lib/sqlalchemy/orm/persistence.py
    index 768c114..3808ab4 100644
    --- a/lib/sqlalchemy/orm/persistence.py
    +++ b/lib/sqlalchemy/orm/persistence.py
    @@ -452,7 +452,7 @@ def _collect_update_commands(
                 params = {}
                 for propkey in set(propkey_to_col).intersection(
                         state.committed_state):
    -                value = state_dict[propkey]
    +                value = state_dict.get(propkey)
                     col = propkey_to_col[propkey]
     
                     if isinstance(value, sql.ClauseElement):
    -- 
    2.5.0

Or perhaps better:

    diff --git a/lib/sqlalchemy/orm/persistence.py 
b/lib/sqlalchemy/orm/persistence.py
    index 768c114..ce6a1f7 100644
    --- a/lib/sqlalchemy/orm/persistence.py
    +++ b/lib/sqlalchemy/orm/persistence.py
    @@ -452,6 +452,9 @@ def _collect_update_commands(
                 params = {}
                 for propkey in set(propkey_to_col).intersection(
                         state.committed_state):
    +                if propkey not in state_dict:
    +                    continue
    +
                     value = state_dict[propkey]
                     col = propkey_to_col[propkey]
     
    -- 
    2.5.0

I checked both changes vs noserun.py on sqlalchmy 1.1b1, they do not cause 
any errors.

However what makes me uneasy is that not everything is very clear to me 
what is happening under the hood. Seems like ORM things that state_dict 
should have published_at because it was set/modified while it's not the 
case. Fact is that if I specify published_at to be some value in my test, 
another field will be reported as KeyError and so on, and so on, till I 
have all fields set.

Please let me know if my patch and my findings are any value for you.

-- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to sqlalchemy+unsubscr...@googlegroups.com.
To post to this group, send email to sqlalchemy@googlegroups.com.
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to