Thank you, Mike! Your answer is really what I want to know, many thanks to you!
On Sep 6, 5:17 am, Michael Bayer <mike...@zzzcomputing.com> wrote: > On Sep 5, 2010, at 2:23 PM, Freewind wrote: > > > > > > > I post the detail class here: > > > Session = scoped_session(sessionmaker()) > > Base = declarative_base() > > > class User(Base): > > __tablename__ = 'users' > > id = Column(Integer, primary_key=True) > > question_count = Column(Integer) > > questions = relationship('Question', backref='user', > > primaryjoin='User.id==Question.user_id') > > > class QuestionCallback(MapperExtension): > > def after_insert(self, mapper, connection, instance): > > user = instance.user > > user.question_count += 1 > > > class Question(Base): > > __tablename__ = 'questions' > > id = Column(Integer, primary_key=True) > > title = Column(String) > > content = Column(String) > > user_id = Column(Integer, ForeignKey('users.id')) > > > Now, a user post a new question, after the question has been inserted > > to database, the 'user.question_count' should be updated too. > > > # load user > > question = Question(title='aa', content='bb', user_id=user_id) > > Session.add(question) > > Session.commit() > > > What I want is: the question is added to database, and after inserted, > > the user of this question should have its 'question_count' column > > updated too. > > > I think the extension should be on the Question class. Because the > > flow is: > > > Insert a question first, then update the associated user. If I write > > 'before_insert()' on user, it won't have chance to be invoked. > > If the "user" row does not already exist, the "question" cannot be inserted > first. It references the "id" column of the "users" table, so the database > would raise a constraint exception if question row referenced a nonexistent > user row. > > Because this relationship is simple and has no per-row dependencies, the unit > of work will perform INSERT or UPDATE statements on the "User" class > unconditionally before any which occur upon "Question". > > Additionally, a key concept of the unit of work pattern is that it organizes > a full list of all INSERT,UPDATE, and DELETE statements which will be > emitted, as well as the order in which they are emitted, before anything > happens. When the before_insert() and after_insert() event hooks are > called, this structure has been determined, and cannot be changed in any way. > The documentation for before_insert() and before_update() mentions that > the flush plan cannot be affected at this point - only individual attributes > on the object at hand, and those which have not been inserted or updated yet, > can be affected here. Any scheme which would like to change the flush plan > must use SessionExtension.before_flush. However, there are several ways of > accomplishing what you want here without modifiying the flush plan. > > The simplest is what I already suggested. Use > MapperExtension.before_insert() on the "User" class, and set > user.question_count = len(user.questions). This assumes that you are > mutating the user.questions collection, rather than working with > Question.user to establish the relationship. If you happened to be using a > "dynamic" relationship (which is not the case here), you'd pull the history > for user.questions and count up what's been appended and removed. > > The next way, is to do pretty much what you think you want here, that is > implement after_insert on Question, but emit the UPDATE statement yourself. > That's why "connection" is one of the arguments to the mapper extension > methods: > > def after_insert(self, mapper, connection, instance): > connection.execute( > users_table.update().\ > > values(question_count=users_table.c.question_count +1).\ > > where(users_table.c.id==instance.user_id) > ) > > I wouldn't prefer that approach since it's quite wasteful for many new > Questions being added to a single User. So yet another option, if > User.questions cannot be relied upon and you'd like to avoid many ad-hoc > UPDATE statements, is to actually affect the flush plan by using > SessionExtension.before_flush: > > class MySessionExtension(SessionExtension): > def before_flush(self, session, flush_context): > for obj in session.new: > if isinstance(obj, Question): > obj.user.question_count +=1 > > for obj in session.deleted: > if isinstance(obj, Question): > obj.user.question_count -= 1 > > To combine the "aggregate" approach of the "before_flush" method with the > "emit the SQL yourself" approach of the after_insert() method, you can also > use SessionExtension.after_flush, to count everything up and emit a single > mass UPDATE statement with many parameters. We're likely well in the realm > of overkill for this particular situation, but I presented an example of such > a scheme at Pycon last year, which you can see at > http://bitbucket.org/zzzeek/pycon2010/src/tip/chap5/sessionextension.py. > > > > > > > On 9月6日, 上午2时05分, Michael Bayer <mike...@zzzcomputing.com> wrote: > >> On Sep 5, 2010, at 1:27 PM, Freewind wrote: > > >>> There are two classes: User and Question > > >>> A user may have many questions, and it also contains a question_count > >>> to record the the count of questions belong to him. > > >>> So, when I add a new question, I want update the question_count of the > >>> user. At first, I do as: > > >>> question = Question(title='aaa', content='bbb') > >>> Session.add(question) > >>> Session.flush() > > >>> user = question.user > >>> ### user is not None > >>> user.question_count += 1 > >>> Session.commit() > > >>> Everything goes well. > > >>> But I wan't to use event callback to do the same thing. As following: > > >>> from sqlalchemy.orm.interfaces import MapperExtension > >>> class Callback(MapperExtension): > >>> def after_insert(self, mapper, connection, instance): > >>> user = instance.user > >>> ### user is None !!! > >>> user.question_count += 1 > > >> Not really sure why question.user would be None here if you had set it (in > >> your example above, its not set, unless you have some odd join condition > >> going on), but it would also be more appropriate here for the extension to > >> be on the User class, not Question, using before_insert(). > > >>> 2. If I change that line to: > > >>> Session.query(User).filter_by(id=instance.user_id).one() > > >>> I can get the user successfully, But: the user can't be updated! > > >>> Look I have modified the user: > > >>> user.question_count += 1 > > >>> But there is no 'update' sql printed in the console, and the > >>> 'question_count' are not updated. > > >> So, if you were to emit the INSERT statements for both your User and your > >> Question, assuming Question has a foreign key to User, which would INSERT > >> would need to occur first ? The "User" row would already have been > >> inserted here by the time your Question after_insert is invoked (and the > >> fact that you can look it up proves it). People usually use > >> before_insert() which states in its doc that you can't manipulate the > >> flush plan or assume it will change in any way at that point, but I guess > >> I'll make this message much more explicit for all the intra-flush() hooks. > > >>> 3. I try to add 'Session.flush()' or 'Session.commit()' in the > >>> 'after_insert()' method, but both cause errors. > > >> The error should be clear here - "Session is already flushing". The > >> extension points are within a transaction, within a flush. Invoking > >> flush() within flush() is not something that is possible, and invoking > >> commit() within flush(), assuming the reentrant flush() issue weren't > >> present, would mean half of the flush is committed, half not, and the > >> transaction would then be incorrectly committed before it was actually > >> committed on the outside.- 隐藏被引用文字 - > > >> - 显示引用的文字 - > > > -- > > You received this message because you are subscribed to the Google Groups > > "sqlalchemy" group. > > To post to this group, send email to sqlalch...@googlegroups.com. > > To unsubscribe from this group, send email to > > sqlalchemy+unsubscr...@googlegroups.com. > > For more options, visit this group > > athttp://groups.google.com/group/sqlalchemy?hl=en.- Hide quoted text - > > - Show quoted text -- Hide quoted text - > > - Show quoted text - -- You received this message because you are subscribed to the Google Groups "sqlalchemy" group. To post to this group, send email to sqlalch...@googlegroups.com. To unsubscribe from this group, send email to sqlalchemy+unsubscr...@googlegroups.com. For more options, visit this group at http://groups.google.com/group/sqlalchemy?hl=en.