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("categories", meta,
  27.     Column("id", Integer(), primary_key=True),
  28.     Column("name", String(100)),
  29.     Column("description", String())
  30. )
  31.  
  32. categoriesfeeds_table = Table("categories_feeds", meta,
  33.     Column("feed_id", Integer(), ForeignKey("feeds.id")),
  34.     Column("category_id", Integer(), ForeignKey("categories.id"))
  35. )

Gdybyśmy nie chcieli używać ORM to powyższe deklaracje wystarczyłyby nam do stworzenia odpowiednich tabel i działania na nich.
Oczywiście chcemy używać ORM zatem definiujemy klasy:

Code (python)
  1. class Feed(object):
  2.     def __str__(self):
  3.         return self.title
  4.     def __repr__(self):
  5.         return self.title
  6.  
  7. class Category(object):
  8.     def __str__(self):
  9.         return self.name
  10.     def __repr__(self):
  11.         return self.name
  12.  
  13. class FeedItem(object):
  14.     def __str__(self):
  15.         return self.title
  16.     def __repr__(self):
  17.         return self.title

Uwaga: Dla tych, którzy nie znają Pythona: metody specjalne __str__() oraz __repr__() odpowiadają za to jak widziane są obiekty np w shellu Pythonowym. Oczywiście nie musimy tego robić w takiej sytuacji zamiast np listy z tytułami zobaczymy coś takiego:

Teraz odpowiednio mapujemy tabele na klasy:

Code (python)
  1. feeditems_mapper = assign_mapper(session_context, FeedItem, feeditems_table,
  2.         order_by=desc(feeditems_table.c.date_modified)
  3. )

session_context to zmienna Pylons zawierająca sesja SQLAlchemy. Kod chyba nie wymaga tłumaczenia poza order_by
tu odwołujemy się bezpośrednio do pola date_modifiedtabeli feeditems

Code (python)
  1. feeds_mapper = assign_mapper(session_context, Feed, feeds_table, properties = {
  2.         "feeditems" : relation(FeedItem, backref="feed")
  3.     }
  4. )

Tworzymy relację między Feed i FeedItem określoną na poziomie tabeli przez Column(”feed_id”, Integer(), ForeignKey(”feeds.id”)). backref to atrybut pod jakim ta relacja będzie dostępna z drugiej strony relacji czyli w obiekcie klasy FeedItem.

Code (python)
  1. categories_mapper = assign_mapper(session_context, Category, categories_table, properties = {
  2.         "feeds" : relation(Feed, secondary=categoriesfeeds_table, lazy=False, backref="categories")
  3.     }
  4. )

Relacja wiele-do-wielu między Category i Feed tu podajemy tabelę łączącą secondary=feedscategories_table. Jeżeli chcemy możemy tu precyzować w jaki sposób to łączenie ma wyglądać używając primaryjoin czyli warunek łączenia Category z tabelą łączącą i secondaryjoin warunek łączenia tabeli łączącej z Feed. W tym przypadku definicje kluczy w tabela nam zupełnie wystarczą. Więcej w tym temacie tu

Główny problem:
Jak zmapować relację FeedItems i Category. Muszę przyznać, że głowiłem się nad tym trochę. Chciałem to zrobić za pomocą primaryjoin i secondaryjoin ale musiałbym złączyć 4 tabele categories, categories_feeds, feeds, feeditems w definicji relacji co stanowiło dla mnie spory problem. Wrzuciłem w tej sprawie pytanie na grupę dyskusyjną sqlalchemy i bardzo szybko dostałem odpowiedź, że popełniam błąd próbując umieścić definicję relacji która nie odzwierciedla relacji w bazie danych.

Rozwiązanie problemu:
Nasze klasy powinny wyglądać tak:

Code (python)
  1. class Category(object):
  2.     def __str__(self):
  3.         return self.name
  4.     def __repr__(self):
  5.         return self.name
  6.     def _get_feeditems(self):
  7.         join = Category.join_to( "feeditems")
  8.         return FeedItem.select_by(Category.c.id==self.id, join)
  9.     feeditems = property(_get_feeditems)

Metoda _get_feeditems() używa joina przez tabelę feeds do feeditems. Można też użyć join_via([”feeds”, “feeditems”]) aby sprecyzować dokładnie ścieżkę joina. Następnie używamy tego joina by ograniczyć to co zwróci select na klasie FeedItem. Po czym mapujemy metodę na atrybut.

Code (python)
  1. class FeedItem(object):
  2.     @classmethod
  3.     def get_by_category(self, category_name):
  4.         join = FeedItem.join_via(["feed", "categories"])
  5.         return FeedItem.select_by(Category.c.name==category_name, join)
  6.  
  7.     def __str__(self):
  8.         return self.title
  9.     def __repr__(self):
  10.         return self.title
  11.  
  12.     def _get_categories(self):
  13.         return self.feed.categories
  14.     categories = property(_get_categories)

Tu możemy zdefiniować metodę klasową używając tego samego schematu postepowania (tu użyłem join_via ale możemy również użyć join_to(”categories”).

Kolejny problem jaki rozwiązano na grupie był nastepujący:
w tym momencie możemy użyć

Code (python)
  1. Feed.select_by(title="first")

a chciałbym pobrać dane tak:

Code (python)
  1. Feed.selct_by(name=["first", "second"])

.
Niestety to nie działa ale można to zrobić tak:

Code (python)
  1. Feed.select(Feed.c.title.in_("first", "second"))

W taki sposób wygrywamy pierwsze starcie z SQLAlchemy :)
Mam nadzieję, że ten przykład zaoszczędzi komuś trochę poszukiwań.

Leave a Reply »