IntegrityError not caught in SQLAlchemy event listener
Question: I'm building a simple database driven blog with Flask and SQLAlchemy. In the
model for the blog postings I define title and slug attributes:

    
    
    class BlogPost(Model):
        ...
        title = Column(String(80))
        slug = Column(String(80), unique=True)
    

Later I use an event listener to automatically create and insert a slug from
the title:

    
    
    @event.listens_for(BlogPost.title, 'set')
    def autoslug(target, value, oldvalue, initiator):
        target.slug = slugify(value)
    

As expected, if I try to add a post to the database, and the title of the post
evaluates to the same slug as a previous post, then the transaction fails with
an IntegrityError. I don't think in practice this will be a problem anyway.
But just for giggles I tried something like this:

    
    
    from sqlalchemy.exc import IntegrityError
    
    @event.listens_for(BlogPost.title, 'set')
    def autoslug(target, value, oldvalue, initiator):
        try:
            target.slug = slugify(value)
        except IntegrityError:
            target.slug = slugify(value) + random_string()
    

`random_string` could be anything, really, the point is that nothing that I've
tried gets executed because the IntegrityError isn't getting caught, and I'm
not sure why - attempting to add & commit a post to the database with the same
title still raises an IntegrityError and aborts the transaction when I try to
commit. I've seen a handful of other posts about it, but the answers are
mostly pretty specific to Pyramid, which I'm not using.

Anybody know what I'm missing here?

Tech involved: Python3, Flask, Flask-Sqlalchemy, Sqlalchemy


Answer: SQLAlchemy will not
[flush](http://docs.sqlalchemy.org/en/latest/orm/session_basics.html#flushing)
changes to model objects to DB when setting. In order to get the error you
have to do something like

    
    
    from sqlalchemy.exc import IntegrityError
    from sqlalchemy.orm.session import object_session
    
    @event.listens_for(BlogPost.title, 'set')
    def autoslug(target, value, oldvalue, initiator):
        session = object_session(target)
        try:
            with session.begin_nested():
                target.slug = slugify(value)
                session.flush()
        except IntegrityError:
            target.slug = slugify(value) + random_string()
    

Note that you have to wrap your possible integrity violation in a nested
transaction (a [savepoint](https://en.wikipedia.org/wiki/Savepoint)), or your
whole transaction will fail even though you catch the `IntegrityError`. If
your DB doesn't support savepoints or an SQLAlchemy implementation of the
idea, you're out of luck.

