When working with database-powered applications, it’s often common to want to have a field on a data model that is a computed rather than static value; “computed” is just a fancy way to say calculated or evaluated on the fly. Developers new to SQLAlchemy tend to go thru a bunch of coding gymnastics, sometimes involving a bunch of private methods on the model to solve this. There’s a better way – Hybrid properties.
Take a look at this sample of a partial class.
class Pokemon(db.Model):
id = db.Column(UUID(as_uuid=True), unique=True, nullable=False, primary_key=True, default=uuid.uuid4)
species_name = db.Column(db.String, unique=True, nullable=False)
nick_name = db.Column(db.String, nullable=True)
@hybrid_property
def name(self):
if self.nick_name is None:
return self.species_name
else:
return self.nick_name + ' - ' + self.species_name
It’s a pretty straight forward model you might see in most Flask applications. If you’ve ever played one of the many Pokemon games, you know that you can give your pokemon nicknames. For example, you can call your Pikachu something like “Sparky” or “Shock Mouse” or really whatever you want. That’s great but you can imagine from a development perspective on say you probably don’t want to have to check if the Pikachu in question has a nickname everywhere you display its name and you probably want to show both the nickname and the regular name without again doing a bunch of “if checking” on your front-end code or in your API response. That’s where our hybrid property comes in. It allows us to encapsulate whatever logic we need to compute the desired in the model itself and reference it in the same way we’d reference any other value on the model. Take a look at these examples of potential queries. Let’s assume we want to find Pikachus but only ones named “Sparky.”
def bad_method_one(self):
pikachus = Pokemon.query.filter_by(species_name="Pikachu")
sparkies = []
for pika in pikachus:
if pika.nick_name is not None:
if pika.nick_name == 'Sparky':
sparkies.append(pika)
else:
print('no op')
else:
print('no nick_name no op')
There’s an obvious problem there in that you are looping thru all of the Pikachus, checking if they actually have a nickname at all and then comparing it to the string we’d like to see. You could of course inverse the search by that’s problematic as well.
def bad_method_two(self):
pikachus = Pokemon.query.filter_by(nick_name="Sparky")
sparkies = []
for pika in pikachus:
if pika.species_name == "Pikachu":
sparkies.append(pika)
else:
print('no op')
By contrast, using our hybrid property we get something much simpler.
pikachus = Pokemon.query.filter_by(name="Sparky - Pikachu")
That’s it! Much cleaner, right? I’ve tried to keep this at a pretty simple here, but there are of course plenty of more complex cases. SQLAlchemy developers with some experience will correctly note that there are other ways that I have not discussed to complete the sample here without using a hybrid property. They are of course correct; I took a simple (and admittedly contrived) case for illustrative purposes.
I hope you have found this helpful, please follow me on Twitter and let me know what you think! If you need any development work done, check out my shop The Mad Botter. Also, subscribe to my podcast!