SQLAlchemy, Elixir and Pylons - round one
Today I checked the Elixir project’s website to see how’s work going and I was really surprised.
Elixir is in it’s beta phase (maybe even alpha) so I assumed that the functionality is limited and that it may be unstable.
After reading the examples I’ve decided to give it a shot in my project described in my last post about SQLAlchemy (only in polish for now - sorry).
I would like to mark that it’s my first round with SQLAlchemy and Pylons so I may have made some stupid mistakes. If you find any please point them out.
My model defined in “plain” SQLAlchemy looked like this:
RSS channel(Feed):
-
feeds_table = Table("feeds", meta,
-
Column("id", Integer(),primary_key=True),
-
Column("title", String(40)),
-
Column("feed_url", String(), default=""),
-
Column("public_url", String(), default=""),
-
Column("is_defunct", Boolean(), default=False),
-
)
-
class Feed(object):
-
def __str__(self):
-
return self.title
-
def __repr__(self):
-
return self.title
-
-
feeds_mapper = assign_mapper(session_context, Feed, feeds_table, properties = {
-
"feeditems" : relation(FeedItem, backref="feed")
-
}
-
)
After the switch to Elixir it looks like this. I think you will agree that this code is better to read:
-
class Feed(Entity):
-
has_field("title", String(40))
-
has_field("feed_url", String, default="")
-
has_field("public_url", String, default="")
-
has_field("is_defunct", Boolean, default=False)
-
has_many("feeditems", of_kind="FeedItem")
-
has_and_belongs_to_many("categories", of_kind="Category", inverse="feeds")
-
using_options(tablename="feeds")
-
def __str__(self):
-
return self.title
-
def __repr__(self):
-
return self.title
Change FeedItems from this:
-
feeditems_table = Table("feed_items", meta,
-
Column("id", Integer(), primary_key=True),
-
Column("feed_id", Integer(), ForeignKey("feeds.id")),
-
Column("guid", String(250)),
-
Column("date_modified", DateTime()),
-
Column("title", String(40)),
-
Column("link", String()),
-
Column("summary", String()),
-
)
-
-
class FeedItem(object):
-
@classmethod
-
def get_by_category(self, category_name):
-
join = FeedItem.join_via(["feed", "categories"])
-
return FeedItem.select_by(Category.c.name==category_name, join)
-
def __str__(self):
-
return self.title
-
def __repr__(self):
-
return self.title
-
def _get_categories(self):
-
return self.feed.categories
-
categories = property(_get_categories)
-
-
feeditems_mapper = assign_mapper(session_context, FeedItem, feeditems_table,
-
order_by=desc(feeditems_table.c.date_modified)
-
)
to this
-
class FeedItem(Entity):
-
has_field("guid", String(250))
-
has_field("date_modified", DateTime())
-
has_field("title", String(40))
-
has_field("link", String())
-
has_field("summary", String())
-
belongs_to("feed", of_kind="Feed")
-
has_and_belongs_to_many("tags", of_kind="Tag", inverse="feeditems")
-
using_options(tablename="feeditems", order_by="-date_modified")
-
-
@classmethod
-
def get_by_category(self, category_name):
-
join = FeedItem.join_via(["feed", "categories"])
-
return FeedItem.select_by(Category.c.name==category_name, join)
-
def __str__(self):
-
return self.title
-
def __repr__(self):
-
return self.title
-
def _get_categories(self):
-
return self.feed.categories
-
categories = property(_get_categories)
Categories used to look like this:
-
categories_table = Table("categories", meta,
-
Column("id", Integer(), primary_key=True),
-
Column("name", String(100)),
-
Column("description", String())
-
)
-
-
categoriesfeeds_table = Table("categories_feeds", meta,
-
Column("feed_id", Integer(), ForeignKey("feeds.id")),
-
Column("category_id", Integer(), ForeignKey("categories.id"))
-
)
-
class Category(object):
-
def __str__(self):
-
return self.name
-
def __repr__(self):
-
return self.name
-
def _get_feeditems(self):
-
join = Category.join_via(["feeds", "feeditems"])
-
return FeedItem.select_by(Category.c.id==self.id, join)
-
feeditems=property(_get_feeditems)
-
-
categories_mapper = assign_mapper(session_context, Category, categories_table, properties = {
-
"feeds" : relation(Feed, secondary=categoriesfeeds_table, lazy=False, backref="categories")
-
}
-
)
now the definition is:
-
class Category(Entity):
-
has_field("name", String(100))
-
has_field("description", String())
-
has_and_belongs_to_many("feeds", of_kind="Feed", inverse="categories")
-
using_options(tablename="categories")
-
def __str__(self):
-
return self.name
-
def __repr__(self):
-
return self.name
-
def _get_feeditems(self):
-
join = Category.join_via(["feeds", "feeditems"])
-
return FeedItem.select_by(Category.c.id==self.id, join)
-
feeditems=property(_get_feeditems)
As you can see in FeedItem definition I’ve added the Tag class which looks this way:
-
class Tag(Entity):
-
has_field("name", String(100), unique=True, index=True)
-
has_and_belongs_to_many("feeditems", of_kind="FeedItem", inverse="tags")
-
using_options(tablename="tags")
I use has_field() do define fields but you can use the alternative which is equivalent:
-
class Feed(Entity):
-
with_fields(
-
title = Field(String(40)),
-
feed_url = Field(String(), default=""),
-
#etc…
-
)
If you don’t define the primary key (primary_key=True), Elixir adds a field named id which becomes the primary key (just like ActiveRecord).
We don’t have to define foreign keys anymore, relation definitions will handle it including secondary tables for many-to-many relations.
There was still one problem - in SQLAlchemy’s definitions we used pylons session_context and meta to “clip” the definitions into Pylons app. Elixir looks for two objects in the current namespace metadata and session. I think it’s related to TurboGears integration. If it doesn’t find metadata it uses it’d own one defined in the module but there is still a problem with session. One solution that I’ve found is to put the database connection in the __call__() method in /lib/base.py but it seemed somehow awkward to me. After playing around I found that all Elixir needs from session is context and this is what we have in pylons.database.session_context.
In a flash: I used a trick. The head of models/__init__.py should look like this:
-
from elixir import *
-
from pylons.database import session_context
-
class FakeSession:
-
def __init__(self, context):
-
self.context = context
-
session = FakeSession(session_context)
If you have another solution please let me know.
In my case everything works fine.
Second thing. If you change the head of the file to:
-
from elixir import *
-
from turbogears.database import metadata, session
This example should work in TurboGears too.
Have fun :)