0

We are implementing a webservice, that displays data from my database with flask and sqlalchemy. The database schema is done, but now we struggle with the implementation of the slugs to display all the elements from the database.

Example of the database relation:

class Parent(Base, db.Model):

    __tablename__ = "parent"

    id = Column(Integer, primary_key=True)
    name = Column(String(80), unique=True, nullable=False)
    slug = Column(String, nullable=False, default=set_slug())

class Child(Base, db.Model):

    __tablename__ = "child"

    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey("parent.id"), nullable=False)
    name = Column(String(80), unique=True, nullable=False)
    slug = Column(String, nullable=False, default=set_slug())

The default value for the slug is just the name, that get's slugified with the slugify method from the slugify package. In the database also exist elements with the same name, but they are different because of their relationship to the parent element. So the slug should be the same, if there are two elements with the same name but a different parent. Also if two elements have the same name and parent, the slug should be unique for both elements.

My first idea was to implement an attribute event for child.name and parent.name whenever a set event is triggered.

from functools import partial
from slugify import slugify

def set_slug(target: Base, value: str, oldvalue: str, initiator: Event, global_unique=False) -> None:
    slug_value = slugify(value)

    # build the database query
    db_query = target.__class__.query
    if not global_unique:
         # case, the slug is local unique
         db_query = db_query.filter(target.__class__.parent_id == target.parent_id )

    all_slugs = [entry.slug for entry in db_query.all()]
    if value and (not target.slug or value != oldvalue):
        if slug_value not in all_slugs:
            # case, the standard slug (name = slug) is available
            target.slug = slug_value
        else:
            # case, the standard slug is already in use
            counter = 1
            while True:
                # iterate as long as no unique slug was found
                new_slug = f"{slug_value}-{counter}"
                if new_slug not in all_slugs:
                    target.slug = new_slug
                    return
                counter += 1

event.listen(Child.name, "set", set_slug, retval=False)
event.listen(Parent.name, "set", partial(set_slug, global_unique=True), retval=False)

The method works overall, if the target object already exists in the database. The problem is, that this doesn't work if the object was created, because every attribute is just None. Is there a way to get the parent_id in that state of the event or do you have any other ideas, how to implement that uniquify?

2 Answers2

0

Slugs can really slow you down. Why not just make them all globally unique? A positive side-effect is that if you change a name then you don't have to revisit the entire uniqueness issue again because the code/id in the slug is already unique.

Using sequences

If you use postgresql you can use sequences for your primary keys which can be created before an object is created.

Then you don't need to involve the parent ids, like this:

child_id = session.execute(childs_seq)
child = Child(id=child_id, slug=f"{slugify(name)}--{child_id}", name=name))
#...

Using random code

Another option, which is probably better, is to just generate a random string and attach it to the slug, you might want to save it to more easily regenerate the slug or for fast lookup and make the column unique. You could generate a code using something like Random string generation with upper case letters and digits

random_code = get_random_code()
child = Child(slug=f"{slugify(name)}--{random_code}", code=random_code, name=name)
#...
Ian Wilson
  • 6,223
  • 1
  • 16
  • 24
0

I've written an article on this topic in which an example can be extended to take into account parent_id. The trick is to use the before_commit event. Take a look here

schumskie
  • 157
  • 1
  • 7