On 12/14/2015 10:47 AM, Drachenfels wrote:
> 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'
there should not be a keyerror here but unfortunately there is not
enough clear information here in order for us to reproduce the issue.
Can you please produce a simple test case following the guidelines at
http://stackoverflow.com/help/mcve (which includes but is not limited
to: single file, no imports outside of SQLA, no extraneous methods or
code, sample data is included, etc) and post as a bug report at
https://bitbucket.org/zzzeek/sqlalchemy/issues ? thanks.
>
> 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 [email protected]
> <mailto:[email protected]>.
> To post to this group, send email to [email protected]
> <mailto:[email protected]>.
> Visit this group at https://groups.google.com/group/sqlalchemy.
> For more options, visit https://groups.google.com/d/optout.
--
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 [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.