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 <[email protected]> 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 [email protected].
> To unsubscribe from this group, send email to 
> [email protected].
> For more options, visit this group at 
> http://groups.google.com/group/sqlalchemy?hl=en.
> 

-- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/sqlalchemy?hl=en.

Reply via email to