Making a Statement with Elixir · Tuesday, February 13, 2007
Recently, we announced the release of Elixir. One of the most powerful features of Elixir is the ability to create your own DSL “statements” to extend your model objects in a cross-cutting way. Elixir provides a limited set of DSL statements for defining fields (has_field, with_fields), relationships (has_one, belongs_to, etc.), and even for defining options. Creating your own statements is a fairly straightforward way to add capabilities to your model objects easily.
Baby Steps…
Creating an Elixir statement is a two step process. First, you need to create a class that implements the statement, and then you need to define the statement itself. A statement class is just a regular class that extends from object which gets passed a Entity class, arguments, and keyword arguments for the statement at processing time. Here is a quick example of a Statement object that will print out whatever is passed to it:
class PrintStatement(object): def __init__(self, entity, *args, **kwargs): print 'PrintsOut statement called:' print ' entity ->', entity print ' args ->', args print ' kwargs ->', kwargs
Now that we have created a simple statement, we need to define the term that we want to use. Terms should describe what the statement does to the Entity that they are being applied to. In this case, we’ll pick the term prints_out:
from elixir.statements import Statement prints_out = Statement(PrintStatement)
Viola! We have created our first Elixir statement. It doesn’t do much, but it should help us understand a bit more about how Elixir statements work. Lets see what happens when we use it from an interactive interpreter. Save the above python code into a file called mystatements.py and fire up your python interpreter:
>>> from elixir import * >>> from mystatements import prints_out >>> >>> class Person(Entity): ... has_field('name', Unicode(64)) ... prints_out('arg 1', 'arg 2', kwarg_one=1, kwarg_two=2) ... PrintsOut statement called: entity -> <class '__main__.Person'> args -> ('arg 1', 'arg 2') kwargs -> {'kwarg_two': 2, 'kwarg_one': 1}
As you can see, statements are processed as Entity objects are defined. This gives you the power to do things like add methods to entities. So, now we have created our first, quite useless statement. I think its safe for us to move on to something more complex.
Tag, You’re It!
Elixir statements are great for implementing cross-cutting behavior, and one very popular kind of cross-cutting behavior these days is “tagging.” Its actually quite simple to implement generic tagging on top of Elixir statements. Lets decide on what we want our API to look like first. It would be nice if we could just say that a model objects acts like it is taggable, and then it would automatically grow the capability to add tags and fetch instances of itself through tags:
class Person(Entity): has_field('name', Unicode) acts_as_taggable() ... some_person_instance.add_tag('cool') ... cool_people = Person.get_by_tag('cool')
That looks nice. Let’s implement it. Lets create a file called taggable.py to put our tagging implementation into. We are going to want to store our tags into the database, so let’s first create an Elixir model object that defines a tag. A Tag object will need to know that an entity has been tagged with a particular tag. So, it will need to know the id of the entity, what table that entity lives in, and the name of the tag:
class Tag(Entity): has_field('target_id', Integer) has_field('target_table', Unicode) has_field('tagname', Unicode)
Great! Now we have a table in the database where we can store tags, so lets create a statement that adds the add_tag and get_by_tags methods to our model objects:
from sqlalchemy import and_ from elixir.statements import Statement class Taggable(object): taggable_entities = [] def __init__(self, entity): Taggable.taggable_entities.append(entity) def add_tag(self, tag): Tag(target_id=self.id, target_table=self.table.name, tagname=tag) def get_by_tag(cls, tag): return entity.select(and_(Tag.c.target_id==entity.c.id, Tag.c.target_table==entity.table.name, Tag.c.tagname==tag)) entity.add_tag = add_tag entity.get_by_tag = classmethod(get_by_tag) acts_as_taggable = Statement(Taggable)
Excellent, now we have a statement that does precisely what we want! Lets walk through that chunk of code quickly, as this is the largest piece of code that we have written yet. Our Taggable class starts out by adding the entity to a list called taggable_entities. We’ll use this piece of information later, so its safe to ignore it for now.
Following this, we define the add_tag method that we will attach to the entity class, which simply creates a tag on an instance of an Entity object, containing the id of the entity, the table name, and the specified tag. We can use this information to fetch this instance from the database by tag later.
Next, we define a get_by_tag class method that we will attach to the passed in Entity class. This method selects all instances of the decorated Entity class by joining on the tag table that we defined earlier using SQLAlchemy’s excellent built-in query support.
Making it Better
So, now we have the ability to tag and fetch single Entity types by tag. What if we want to fetch all entities, regardless of type, by tag? Well, we have a little bit more work to do, but not much. Lets add a class method to our Taggable class that can do this for us. Put this into your Taggable class below the constructor:
@classmethod def get(cls, tag): instances = [] for entity in Taggable.taggable_entities: instances.extend(entity.get_by_tag(tag)) return instances
There, that was easy! Now we can simply call Taggable.get('some_tag') to get all instances of any taggable entity that are tagged with ‘some_tag’. This class method simply loops across the list of taggable entities that we defined earlier, and simply calls their get_by_tag method to fetch the desired objects from the database.
Now, we are able to fetch multiple kinds of objects using tags. Here is a short example of what it might look like:
class Person(Entity): has_field('name', Unicode) acts_as_taggable() class Movie(Entity): has_field('name', Unicode) acts_as_taggable() ... jonathan = Person.get_by(name='Jonathan LaCour') blade_runner = Movie.get_by(name='Blade Runner') jonathan.add_tag('awesome') blade_runner.add_tag('awesome') ... # fetch all Movie and Person objects tagged with 'awesome' awesome_objects = Taggable.get('awesome') for obj in awesome_objects: print obj.name
Cool! Please note that this implementation of tagging is fairly naive, and could be made quite a bit better with more work. But, it certainly does show how easy it is to create your own Elixir statements that provide cross-cutting functionality. I have uploaded a slightly improved taggable plugin, along with some example code of how to use it. Enjoy!
Wrapping Up
Elixir is a really great toolkit, largely thanks to the power of SQLAlchemy. The ability to define your own DSL statements using Elixir opens up a lot of possibilities for creating highly readable extensions to your model objects.
Comment
- OMG this is wonderful. Excuse me while I go re-write a lot of things I was doing the hard way. Thank you!
— nerkles 1263 days ago # - By the way, you can do something similar to this for non-Elixir classes using “class decorators” as implemented by the DecoratorTools package. The main difference is that class decorators are applied in the opposite order from Elixir Statement objects. But they can be used with any class, Elixir-based or otherwise.
— Phillip J. Eby 1262 days ago # - Philip: Very cool. It figures that there is something that already does this in PEAK, as every time I think I have found something unique, it usually ends up being in PEAK as well :) The good news is that the code that implements class decorators is extremely short, so it doens’t really bother me that we have Elixir-specific code for this (at least for now).
— Jonathan LaCour 1261 days ago #
commenting closed for this article