19 March 2007

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):

Code (python)
  1. feeds_table = Table("feeds", meta,
  2.     Column("id", Integer(),primary_key=True),
  3.     Column("title", String(40)),
  4.     Column("feed_url", String(), default=""),
  5.     Column("public_url", String(), default=""),
  6.     Column("is_defunct", Boolean(), default=False),
  7. )
  8. class Feed(object):
  9.     def __str__(self):
  10.         return self.title
  11.     def __repr__(self):
  12.         return self.title
  13.  
  14. feeds_mapper = assign_mapper(session_context, Feed, feeds_table, properties = {
  15.     "feeditems" : relation(FeedItem, backref="feed")
  16.     }
  17. )

After the switch to Elixir it looks like this. I think you will agree that this code is better to read:

Code (python)
  1. class Feed(Entity):
  2.     has_field("title", String(40))
  3.     has_field("feed_url", String, default="")
  4.     has_field("public_url", String, default="")
  5.     has_field("is_defunct", Boolean, default=False)
  6.     has_many("feeditems", of_kind="FeedItem")
  7.     has_and_belongs_to_many("categories", of_kind="Category", inverse="feeds")
  8.     using_options(tablename="feeds")
  9.     def __str__(self):
  10.         return self.title
  11.     def __repr__(self):
  12.         return self.title

Change FeedItems from this:

Code (python)
  1. feeditems_table = Table("feed_items", meta,
  2.     Column("id", Integer(), primary_key=True),
  3.     Column("feed_id", Integer(), ForeignKey("feeds.id")),
  4.     Column("guid", String(250)),
  5.     Column("date_modified", DateTime()),
  6.     Column("title", String(40)),
  7.     Column("link", String()),
  8.     Column("summary", String()),
  9. )
  10.  
  11. class FeedItem(object):
  12.     @classmethod
  13.     def get_by_category(self, category_name):
  14.         join = FeedItem.join_via(["feed", "categories"])
  15.         return FeedItem.select_by(Category.c.name==category_name, join)
  16.      def __str__(self):
  17.         return self.title
  18.      def __repr__(self):
  19.         return self.title
  20.      def _get_categories(self):
  21.         return self.feed.categories
  22.     categories = property(_get_categories)
  23.  
  24. feeditems_mapper = assign_mapper(session_context, FeedItem, feeditems_table,
  25.                                  order_by=desc(feeditems_table.c.date_modified)
  26. )

to this

Code (python)
  1. class FeedItem(Entity):
  2.     has_field("guid", String(250))
  3.     has_field("date_modified", DateTime())
  4.     has_field("title", String(40))
  5.     has_field("link", String())
  6.     has_field("summary", String())
  7.     belongs_to("feed", of_kind="Feed")
  8.     has_and_belongs_to_many("tags", of_kind="Tag", inverse="feeditems")
  9.     using_options(tablename="feeditems", order_by="-date_modified")
  10.  
  11.     @classmethod
  12.     def get_by_category(self, category_name):
  13.         join = FeedItem.join_via(["feed", "categories"])
  14.         return FeedItem.select_by(Category.c.name==category_name, join)
  15.     def __str__(self):
  16.         return self.title
  17.     def __repr__(self):
  18.         return self.title
  19.     def _get_categories(self):
  20.         return self.feed.categories
  21.     categories = property(_get_categories)

Categories used to look like this:

Code (python)
  1. categories_table = Table("categories", meta,
  2.     Column("id", Integer(), primary_key=True),
  3.     Column("name", String(100)),
  4.     Column("description", String())
  5. )
  6.  
  7. categoriesfeeds_table = Table("categories_feeds", meta,
  8.     Column("feed_id", Integer(), ForeignKey("feeds.id")),
  9.     Column("category_id", Integer(), ForeignKey("categories.id"))
  10. )
  11. class Category(object):
  12.     def __str__(self):
  13.         return self.name
  14.     def __repr__(self):
  15.         return self.name
  16.     def _get_feeditems(self):
  17.         join = Category.join_via(["feeds", "feeditems"])
  18.         return FeedItem.select_by(Category.c.id==self.id, join)
  19.     feeditems=property(_get_feeditems)
  20.  
  21. categories_mapper = assign_mapper(session_context, Category, categories_table, properties = {
  22.     "feeds" : relation(Feed, secondary=categoriesfeeds_table, lazy=False, backref="categories")
  23.     }
  24. )

now the definition is:

Code (python)
  1. class Category(Entity):
  2.     has_field("name", String(100))
  3.     has_field("description", String())
  4.     has_and_belongs_to_many("feeds", of_kind="Feed", inverse="categories")
  5.     using_options(tablename="categories")
  6.     def __str__(self):
  7.         return self.name
  8.     def __repr__(self):
  9.         return self.name
  10.     def _get_feeditems(self):
  11.         join = Category.join_via(["feeds", "feeditems"])
  12.         return FeedItem.select_by(Category.c.id==self.id, join)
  13.     feeditems=property(_get_feeditems)

As you can see in FeedItem definition I’ve added the Tag class which looks this way:

Code (python)
  1. class Tag(Entity):
  2.     has_field("name", String(100), unique=True, index=True)
  3.     has_and_belongs_to_many("feeditems", of_kind="FeedItem", inverse="tags")
  4.     using_options(tablename="tags")

I use has_field() do define fields but you can use the alternative which is equivalent:

Code (python)
  1. class Feed(Entity):
  2.     with_fields(
  3.         title = Field(String(40)),
  4.         feed_url = Field(String(), default=""),
  5.         #etc…
  6.     )

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:

Code (python)
  1. from elixir import *
  2. from pylons.database import session_context
  3. class FakeSession:
  4.     def __init__(self, context):
  5.         self.context = context
  6. 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:

Code (python)
  1. from elixir import *
  2. from turbogears.database import metadata, session

This example should work in TurboGears too.
Have fun :)

Comments Comments | Categories: pylons, sqlalchemy, turbogears, db, elixir, en | Autor: Adam Hościło




12 March 2007

SQLAlchemy, Elixir i Pylons - przypadkowe starcie.

Wszedłem dziś na strone projektu Elixir by sprawdzić postępy i bardzo się zdziwiłem. Projekt jest w bardzo wczesnej (0.2) fazie beta więc zakładałem, że funkcjonalność jest bardzo ograniczona, a działanie niestabilne. Po przeczytaniu tutoriala i przykładów stwierdziłem, że projekt ma już wszystko czego potrzebuję i mogę spróbować go użyć do aplikacji opisanej w poprzednim wpisie o SQLAlchemy.

Teraz moje modele wyglądają tak:
Kanały RSS(Feed):
Zamiast tego:

Code (python)
  1. feeds_table = Table("feeds", meta,
  2.     Column("id", Integer(),primary_key=True),
  3.     Column("title", String(40)),
  4.     Column("feed_url", String(), default=""),
  5.     Column("public_url", String(), default=""),
  6.     Column("is_defunct", Boolean(), default=False),
  7. )
  8. class Feed(object):
  9.     def __str__(self):
  10.         return self.title
  11.     def __repr__(self):
  12.         return self.title
  13.  
  14. feeds_mapper = assign_mapper(session_context, Feed, feeds_table, properties = {
  15.     "feeditems" : relation(FeedItem, backref="feed")
  16.     }
  17. )

Dostajemy moim zdaniem dużo bardziej przyjazny kod:

Code (python)
  1. class Feed(Entity):
  2.     has_field("title", String(40))
  3.     has_field("feed_url", String, default="")
  4.     has_field("public_url", String, default="")
  5.     has_field("is_defunct", Boolean, default=False)
  6.     has_many("feeditems", of_kind="FeedItem")
  7.     has_and_belongs_to_many("categories", of_kind="Category", inverse="feeds")
  8.     using_options(tablename="feeds")
  9.     def __str__(self):
  10.         return self.title
  11.     def __repr__(self):
  12.         return self.title

Zmieniamy Wpisy RSS (FeedItem):

Code (python)
  1. feeditems_table = Table("feed_items", meta,
  2.     Column("id", Integer(), primary_key=True),
  3.     Column("feed_id", Integer(), ForeignKey("feeds.id")),
  4.     Column("guid", String(250)),
  5.     Column("date_modified", DateTime()),
  6.     Column("title", String(40)),
  7.     Column("link", String()),
  8.     Column("summary", String()),
  9. )
  10.  
  11. class FeedItem(object):
  12.     @classmethod
  13.     def get_by_category(self, category_name):
  14.         join = FeedItem.join_via(["feed", "categories"])
  15.         return FeedItem.select_by(Category.c.name==category_name, join)
  16.      def __str__(self):
  17.         return self.title
  18.      def __repr__(self):
  19.         return self.title
  20.      def _get_categories(self):
  21.         return self.feed.categories
  22.     categories = property(_get_categories)
  23.  
  24. feeditems_mapper = assign_mapper(session_context, FeedItem, feeditems_table,
  25.                                  order_by=desc(feeditems_table.c.date_modified)
  26. )

w

Code (python)
  1. class FeedItem(Entity):
  2.     has_field("guid", String(250))
  3.     has_field("date_modified", DateTime())
  4.     has_field("title", String(40))
  5.     has_field("link", String())
  6.     has_field("summary", String())
  7.     belongs_to("feed", of_kind="Feed")
  8.     has_and_belongs_to_many("tags", of_kind="Tag", inverse="feeditems")
  9.     using_options(tablename="feeditems", order_by="-date_modified")
  10.  
  11.     @classmethod
  12.     def get_by_category(self, category_name):
  13.         join = FeedItem.join_via(["feed", "categories"])
  14.         return FeedItem.select_by(Category.c.name==category_name, join)
  15.     def __str__(self):
  16.         return self.title
  17.     def __repr__(self):
  18.         return self.title
  19.     def _get_categories(self):
  20.         return self.feed.categories
  21.     categories = property(_get_categories)

Poprzednia definicja kategorii:

Code (python)
  1. categories_table = Table("categories", meta,
  2.     Column("id", Integer(), primary_key=True),
  3.     Column("name", String(100)),
  4.     Column("description", String())
  5. )
  6.  
  7. categoriesfeeds_table = Table("categories_feeds", meta,
  8.     Column("feed_id", Integer(), ForeignKey("feeds.id")),
  9.     Column("category_id", Integer(), ForeignKey("categories.id"))
  10. )
  11. class Category(object):
  12.     def __str__(self):
  13.         return self.name
  14.     def __repr__(self):
  15.         return self.name
  16.     def _get_feeditems(self):
  17.         join = Category.join_via(["feeds", "feeditems"])
  18.         return FeedItem.select_by(Category.c.id==self.id, join)
  19.     feeditems=property(_get_feeditems)
  20.  
  21. categories_mapper = assign_mapper(session_context, Category, categories_table, properties = {
  22.     "feeds" : relation(Feed, secondary=categoriesfeeds_table, lazy=False, backref="categories")
  23.     }
  24. )

zmienia się w:

Code (python)
  1. class Category(Entity):
  2.     has_field("name", String(100))
  3.     has_field("description", String())
  4.     has_and_belongs_to_many("feeds", of_kind="Feed", inverse="categories")
  5.     using_options(tablename="categories")
  6.     def __str__(self):
  7.         return self.name
  8.     def __repr__(self):
  9.         return self.name
  10.     def _get_feeditems(self):
  11.         join = Category.join_via(["feeds", "feeditems"])
  12.         return FeedItem.select_by(Category.c.id==self.id, join)
  13.     feeditems=property(_get_feeditems)

Jak widać w definicji FeedItem wzbogaciłem nasze modele o klasę Tag, która wygląda tak:

Code (python)
  1. class Tag(Entity):
  2.     has_field("name", String(100), unique=True, index=True)
  3.     has_and_belongs_to_many("feeditems", of_kind="FeedItem", inverse="tags")
  4.     using_options(tablename="tags")

Używam has_field() do definiowania pól, zamiast tego można używać takiej formy:

Code (python)
  1. class Feed(Entity):
  2.     with_fields(
  3.         title = Field(String(40)),
  4.         feed_url = Field(String(), default=""),
  5.         #etc…
  6.     )

Dodatkowo Elixir dodaje nam pole id, które staje się kluczem podstawowym, jeżeli w definicji żaden z atrybutów kluczem podstawowym nie jest (primary_key=True).
Nie musimy już w definicji tabeli podawać pola jako klucz obcy, definicje relacji zajmą się tym za nas. Dodatkowo stworzona zostanie też tabela łącząca dla relacji wiele-do-wielu.

Jest tylko jeden problem. Używając schematów SQLAlchemy, “spinaliśmy” wszystkie mapowania z sesją SQLAlchemy przez session_context i meta Elixir automatycznie wyszukuje w przestrzeni nazw, w której znajdują się definicje 2 zmiennych (modułów) metadata i session. Jest to chyba związane z ułatwieniem używania w TyrboGears. metadata może być zainicjowana przez samego Elixira ale zostaje problem spięcia go z sesją. Wyszukałem, że można to zrobić umieszczając połączenie z bazą w /lib/base.py w metodzie __call__() ale nie wydało mi się to najładniejszym rozwiązaniem. Po kilku próbach wydedukowałem, że Elixir z session potrzebuje tylko session.context a ten kontekst mamy dostępny przez pylons.database.session_context. W skrócie: problem rozwiązałem następującym “trikiem”(tak powinien wyglądać nagłówek pliku models/__init__.py:

Code (python)
  1. from elixir import *
  2. from pylons.database import session_context
  3. class FakeSession:
  4.     def __init__(self, context):
  5.         self.context = context
  6. session = FakeSession(session_context)

Jeżeli ktoś zna inne rozwiązanie tego problemu, chętnie je poznam.
W moim przypadku wszystko działa bez najmniejszego problemu.
Jeszcze jedna uwaga. Jeżeli powyższy nagłówek zamienimy na :

Code (python)
  1. from elixir import *
  2. from turbogears.database import metadata, session

Powyższe modele powinny bez problemu działać w TurboGears.

Comments 2 Comments | Categories: pylons, python, sqlalchemy, turbogears, db, elixir | Autor: Adam Hościło




11 March 2007

SQLAlchemy - pierwsze starcie.

W ramach poznawania Pylons postanowiłem napisać prostą aplikację, która agregowałaby wpisy z różnych kanałów RSS. Spora część pracy została już zrobiona przy projekcie djangowebsite, kod jest otwarty zatem postanowiłem użyć jego części przepisując go w Pylons z użyciem SQLAlchemy. Kod źródłowy aplikacji umieszczę gdy tylko zakończę prace nad nią.
Nie chcę wgłębiać się tu w podstawy SQLAlchemy zakładam, że przeczytanie tutoriala nie sprawi nikomu kłopotu

Kilka słów o SQLAlchemy.

SQLAlchemy to faktycznie potężne narzędzie. Składa sie z dwóch wartstw: abstrakcyjnej SQL-Python oraz ORM czyli mapującej rekordy z bazy na obiekty Pythona - w uproszczeniu.

Pomimo tego, że projekt jest stosunkowo młody zyskał ogromną popularność i jest używany (lub będzie) w większości webframeworków Pythonowych. W Pylons z SQLAlchemy można korzystać już od dawna w stosunkowo prosty sposób.

Uwaga: Pamiętaj, że to jest moje pierwsze starcie z SQLAlchemy i mogłem popełnić błędy, jeśli takie zauważysz proszę skomentuj je.

Problem.

Mamy trzy modele: kanały RSS (Feed), wpisy z RSS (FeedItem) oraz kategorie (Category). Kanał może mieć wiele wpisów, kanał może mieć wiele kategorii, kategorie mogą mieć wiele kanałów, pośrednio wpisy mogą mieć wiele kategorii.

W SQLAlchemy proces wygląda następująco: tworzymy definicje table, po czym definicje ORM i je na siebie mapujemy.

Definicje tabel:

Code (python)
  1.  
  2. from sqlalchemy import *
  3. from sqlalchemy.ext.assignmapper import assign_mapper
  4. from pylons.database import session_context
  5.  
  6. meta = DynamicMetaData()
  7.  
  8. feeds_table = Table("feeds", meta,
  9.     Column("id", Integer(),primary_key=True),
  10.     Column("title", String(40)),
  11.     Column("feed_url", String(), default=""),
  12.     Column("public_url", String(), default=""),
  13.     Column("is_defunct", Boolean(), default=False),
  14. )
  15.  
  16. feeditems_table = Table("feed_items", meta,
  17.     Column("id", Integer(), primary_key=True),
  18.     Column("feed_id", Integer(), ForeignKey("feeds.id")),
  19.     Column("guid", String(250)),
  20.     Column("date_modified", DateTime()),
  21.     Column("title", String(40)),
  22.     Column("link", String()),
  23.     Column("summary", String()),
  24. )
  25.  
  26. categories_table = Table(&q