Using model callbacks in SQLAlchemy to generate slugs

Feb 7th, 2017 in  by Michael Cho

Dive into SQLAlchemy's little known `event.listen` method to automatically generate slugs when saving a model instance.

When working with Python's SQLAlchemy I find myself missing Rails' active record callbacks, which were an easy way to insert hooks before and after saving any database records. 

One scenario where this is especially useful is for generating slugs, a URL-valid and SEO friendly version of a blog post title for example. If I have a BlogPost model with a title field, I want a BlogPost instance with a title "Dive into Python" to be automatically saved with a slug "dive-into-python". 

This can be accomplished using the event.listen method from SQLAlchemy. Here is an example:


from sqlalchemy import event
from slugify import slugify

from models import db, BaseModel


class BlogPost(BaseModel):
    __tablename__ = 'blog_posts'
    post_id = db.Column(db.Integer, primary_key=True)
    post_title = db.Column(db.String(140))
    slug = db.Column(db.String(140))
    post_content = db.Column(db.Text)
    create_time = db.Column(db.DateTime, server_default=db.func.current_timestamp())

    @staticmethod
    def generate_slug(target, value, oldvalue, initiator):
        if value and (not target.slug or value != oldvalue):
            target.slug = slugify(value)

event.listen(BlogPost.post_title, 'set', BlogPost.generate_slug, retval=False)

This creates a listener so that everytime `post_title` is set, the `BlogPost.generate_slug` method will be called. The `target` in this method is the instance object, and the (new) `value` and `oldvalue` are also available.

I have set the `generate_slug` method to use python-slugify to generate the slugs.

Of course, if you have many models which need slugs you could also move this method into the BaseModel class which BlogPost is inheriting from instead.


Other articles you may like

SQLAlchemy commit(), flush(), expire(), refresh(), merge() - what's the difference?
Nov 2nd, 2017
Prioritized Code Review Checklist - what to look for first, second, and last
Sep 21st, 2017
Many to many relationships in SQLAlchemy models (Flask)
Jul 28th, 2017
Bash script to relink alembic migrations
Jun 12th, 2017
Overriding default Jinja dictionary attributes in Python view templates
Jan 29th, 2017