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.

2 Responses to

  1. ex says:

    Mój ty zbawco :P

    Nie jestem python wyjadaczem, ale troche w nim pisze różnych skryptow/programow (administracyjne itp).
    Potrzebowalem w koncu WWW kilka stron zrobić, po kolei odrzucałem nie odpowiadające mi technologie (django, tg) by skończyć na pylons+sqlalchemy+elixir. Ale miałem właśnie ten problem spięcia obiektów z sesją, widziałem 2a rozwiązania, ale jakoś mi się nie podobały i na tydzień zarzuciłem prace.

    A tu nagle - krótkie i zwięzłe rozwiązanie ;)

    Dzięki

  2. Adam Hościło says:

    Niezmiernie cieszę się, że komuś pomogłem.
    Sporo szukałem jak ładnie zintegrowac Elixira z Pylons bez większej rzeźby używając dostępnych w Pylons rozwiązań.
    Szczerze mówiac to myślałem, że Elixir jest w takiej fazie, że nie ma sensu nawet myśleć o jego używaniu a tu taka niespodzianka. Do tego nie zauważyłem aby spadła wydajność czy pojawiały się błedy.

    Już niedługo coś o AuthKit (jestem w trakcie rozgryzania), pozniej o XLM-RPC

Leave a Reply »