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:
-
-
from sqlalchemy import *
-
from sqlalchemy.ext.assignmapper import assign_mapper
-
from pylons.database import session_context
-
-
meta = DynamicMetaData()
-
-
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),
-
)
-
-
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()),
-
)
-
-
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"))
-
)
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:
-
class Feed(object):
-
def __str__(self):
-
return self.title
-
def __repr__(self):
-
return self.title
-
-
class Category(object):
-
def __str__(self):
-
return self.name
-
def __repr__(self):
-
return self.name
-
-
class FeedItem(object):
-
def __str__(self):
-
return self.title
-
def __repr__(self):
-
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:
-
feeditems_mapper = assign_mapper(session_context, FeedItem, feeditems_table,
-
order_by=desc(feeditems_table.c.date_modified)
-
)
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
-
feeds_mapper = assign_mapper(session_context, Feed, feeds_table, properties = {
-
"feeditems" : relation(FeedItem, backref="feed")
-
}
-
)
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.
-
categories_mapper = assign_mapper(session_context, Category, categories_table, properties = {
-
"feeds" : relation(Feed, secondary=categoriesfeeds_table, lazy=False, backref="categories")
-
}
-
)
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:
-
class Category(object):
-
def __str__(self):
-
return self.name
-
def __repr__(self):
-
return self.name
-
def _get_feeditems(self):
-
join = Category.join_to( "feeditems")
-
return FeedItem.select_by(Category.c.id==self.id, join)
-
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.
-
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)
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ć
-
Feed.select_by(title="first")
a chciałbym pobrać dane tak:
-
Feed.selct_by(name=["first", "second"])
.
Niestety to nie działa ale można to zrobić tak:
-
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ń.


