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.