Elixir - łatwy sposób na SQLAlchemy
Elixir to nakładka na ORMa SQLAlchemy pozwalająca za pomocą klas opisywać tabele w relacyjnych bazach danych. W porównaniu do API dostępnego w samym SQLAlchemy wersja dostępna w Eliksirze stosuje wzorzec Active Record i może być czytelniejsza/łatwiejsza do zastosowania.
Prosty model na dobry początek
Eliksira przedstawię za pomocą kilku części przedstawiających kolejne fragmenty tej biblioteki. Na początek tworzymy prostą klasę opisującą tabelę w bazie. Stwórz plik model.py a w nim taki oto kod:from elixir import *
metadata.bind = "sqlite:///movies.sqlite"
metadata.bind.echo = True
class Movie(Entity):
title = Field(Unicode(30))
year = Field(Integer)
description = Field(UnicodeText)
def __repr__(self):
return '<Movie "%s" (%d)>' % (self.title, self.year)
- title: pole tekstowe, do 30 znaków; na tytuł filmu
- year: pole liczbowe; przechowujące rok wydania filmu
- description: pole tekstowe; na opis lub podobną zawartość
Metoda __repr__ jest opcjonalna i służy do wyświetlania obiektów w czytelnej dla ludzi postaci.
Mając gotowy model warto go użyć. Stwórz drugi skrypt o kodzie (lub wykorzystaj interaktywny interpreter Pythona):from model import *
setup_all()
create_all()
Wywołanie setup_all tworzy obiekt SQLAlchemy typu Table jak i obiekt Mapper dla naszej klasy "Movie". Obiekt "Table" jest pythonowym obiektem opisującym wygląda tabeli w bazie danych. Mapper jest obiektem, który opisuje jak twoja klasa mapuje się na tą tabelę. Oba te obiekty tworzone są automatycznie.
Natomiast create_all spowoduje stworzenie wszystkich brakujących tabel w bazie danych - tabel opisanych przez nasze modele. W tym przypadku zobaczymy że Elixir wykonał takie zapytanie:
CREATE TABLE model_movie (
id INTEGER NOT NULL,
title VARCHAR(30),
year INTEGER,
description TEXT,
PRIMARY KEY (id)
)
Domyślnie Eliksir nazywa tabele wykorzystując nazwę modułu i klasy (co można oczywiście zmienić, ale o tym później). Co ważne to obecność pola id, które jest także kluczem głównym w tej tabeli. Kolumna ta została stworzona bo nie zdefiniowaliśmy żadnego własnego pola z kluczem głównym.
Czas coś dodać do bazy danych. Na początek stwórzmy taki oto obiekt Movie:from model import *
setup_all()
create_all()
m = Movie(title=u"Blade Runner", year=1982)
print m
from model import *
setup_all()
create_all()
m = Movie(title=u"Blade Runner", year=1982)
session.commit()
print Movie.query.all()
Tak oto dodaliśmy pierwszy rekord do bazy za pomocą Eliksira jak i pobraliśmy wszystkie (aż jeden) rekordy za pomocą metody query.
Możemy też w prosty sposób modyfikować istniejące rekordy:from model import *
setup_all()
create_all()
# .first() is equivalent to (but nicer than) .all()[0]
movie = Movie.query.first()
movie.year = 1983
session.commit()
print Movie.query.all()
Pobieramy pierwszy (jedyny jak na razie) rekord, który mapowany jest do obiektu Movie. Możemy np. zmienić rok i wykonać zmiany za pomocą metody commit. Kasowanie odbywa się za pomocą metody delete.
Proste relacje tabel w Eliksirze
Teraz zajmiemy się prostymi relacjami - jeden do wielu i wiele do jednego. Skasuj istniejący plik z bazą SQLite, bo zaczynamy z nieco innymi modelami:
from elixir import *
metadata.bind = "sqlite:///movies.sqlite"
metadata.bind.echo = True
class Movie(Entity):
title = Field(Unicode(30))
year = Field(Integer)
description = Field(UnicodeText)
director = ManyToOne('Director') # <-- add this line
def __repr__(self):
return '<Movie "%s" (%d)>' % (self.title, self.year)
class Director(Entity):
name = Field(Unicode(60))
movies = OneToMany('Movie') # <-- and this one
def __repr__(self):
return '<Director "%s">' % self.name
Doszedł nam nowy model "Director" oraz relacja. Jeden do wielu w "Director" i wiele do jednego w "Movie". Oznacza to że jedna osoba może być reżyserem wielu filmów, czyli film może mieć jednego reżysera. W odróżnieniu od np. modeli Django tutaj relacja definiowana jest w obu modelach (z obu stron).
Czas stworzyć tabele. Można zrobić to jak poprzednio za pomocą create_all, lub też skrótem:from model import *
setup_all(True)
CREATE TABLE model_director (
id INTEGER NOT NULL,
name VARCHAR(60),
PRIMARY KEY (id)
)
CREATE TABLE model_movie (
id INTEGER NOT NULL,
title VARCHAR(30),
year INTEGER,
description TEXT,
director_id INTEGER,
PRIMARY KEY (id),
CONSTRAINT model_movie_director_id_fk FOREIGN KEY(director_id) REFERENCES model_director (id)
)
Jak widzimy w tabeli model_movie stworzona została m.in. kolumna director_id odpowiedzialna za tą relację.
Teraz dla przykładu stwórzmy kilka filmów, reżyserów i powiążmy ich w relacji - każdy film będzie miał swojego reżysera:
from model import *
setup_all(True)
# add directors
rscott = Director(name=u"Ridley Scott")
glucas = Director(name=u"George Lucas")
# add movies
alien = Movie(title=u"Alien", year=1979)
swars = Movie(title=u"Star Wars", year=1977)
brunner = Movie(title=u"Blade Runner", year=1982)
# bind director to movies
# like so:
rscott.movies.append(brunner)
rscott.movies.append(alien)
# or so:
swars.director = glucas
# show the result
print glucas.movies
# commit to database
session.commit()
Jak widać relację można tworzyć na dwa sposoby. Dla modelu mającego relację jeden do wielu dodajemy elementy jak do listy. W przypadku relacji wiele do jednego po prostu przypisujemy obiekt do pola.
Pobieranie danych z bazy danych
We wcześniejszych przykładach stosowaliśmy np. Movie.query.all() by pobrać rekordy z bazy. Atrybut query daje nam obiekt typu Query ze SQLAlchemy. Pozwala on nam na różnorakie formułowanie zapytań za pomocą składni Pythona. Oto przykłady:
from model import *
setup_all(True)
print Movie.query.filter_by(title=u"Alien").one()
print Movie.query.filter(Movie.year > 1980).all()
print Movie.query.filter(Movie.director.has(name=u'Ridley Scott')).all()
print Movie.query.filter(Movie.director.has(Director.name.endswith(u'Scott'))).all()
<Movie "Alien" (1979)>
[<Movie "Blade Runner" (1982)>]
[<Movie "Alien" (1979)>, <Movie "Blade Runner" (1982)>]
[<Movie "Alien" (1979)>, <Movie "Blade Runner" (1982)>]
Relacje wiele do wielu
Kolejny element Eliksira to relacje wiele do wielu jak i złożone klucze. Na początek załóżmy że chcemy dodać kategorie do filmów ("Genre"). Film może należeć do wielu kategorii, a dana kategoria może zawierać również wiele filmów. Mamy relację wiele do wielu.
Drugi element na naszej liście to brak domyślnego klucza głównego. Załóżmy że chcemy dopuszczać filmy o tym samym tytule, ale wydane w różnych latach. By to osiągnąć potrzebujemy złożony klucz główny (composite primary key) - na tytule i roku wydania.
Usuń plik bazy danych i zmodyfikuj modele do postaci:from elixir import *
metadata.bind = "sqlite:///movies.sqlite"
metadata.bind.echo = True
class Genre(Entity):
name = Field(Unicode(15), primary_key=True)
movies = ManyToMany('Movie')
def __repr__(self):
return '<Genre "%s">' % self.name
class Movie(Entity):
title = Field(Unicode(30), primary_key=True) # <-- modify this line
year = Field(Integer, primary_key=True) # <-- and this one
description = Field(UnicodeText)
director = ManyToOne('Director')
genres = ManyToMany('Genre') # <-- and add this one
def __repr__(self):
return '<Movie "%s" (%d)>' % (self.title, self.year)
class Director(Entity):
name = Field(Unicode(60))
movies = OneToMany('Movie') # <-- and this one
def __repr__(self):
return '<Director "%s">' % self.name
Pojawił się model Genre z relacją wiele do wielu do modelu Movie. Podobna definicja znalazła sie w modelu Movie. Dodatkowo title i year zostały zdefiniowane jako klucze główne - poprzez primary_key.
Przy tworzeniu tabel zauważymy że stworzona zostanie tabela pośrednicząca w relacji wiele do wielu:CREATE TABLE model_movie_genres__model_genre_movies (
model_genre_name VARCHAR(15) NOT NULL,
model_movie_title VARCHAR(30) NOT NULL,
model_movie_year INTEGER NOT NULL,
PRIMARY KEY (model_genre_name, model_movie_title, model_movie_year),
CONSTRAINT model_genre_movies_fk FOREIGN KEY(model_genre_name) REFERENCES model_genre (name),
CONSTRAINT model_movie_genres_fk FOREIGN KEY(model_movie_title, model_movie_year) REFERENCES model_movie (title, year)
)
from model import *
setup_all(True)
scifi = Genre(name=u"Science-Fiction")
rscott = Director(name=u"Ridley Scott")
glucas = Director(name=u"George Lucas")
alien = Movie(title=u"Alien", year=1979, director=rscott, genres=[scifi, Genre(name=u"Horror")])
brunner = Movie(title=u"Blade Runner", year=1982, director=rscott, genres=[scifi])
swars = Movie(title=u"Star Wars", year=1977, director=glucas, genres=[scifi])
session.commit()
from model import *
setup_all(True)
print Movie.query.filter(Movie.genres.any(name=u"Horror")).all()
Dodatkowe opcje
Za pomocą dodatkowych opcji możemy np. podać własną nazwę dla tabeli:
class Movie(Entity):
using_options(tablename='movies')
title = Field(Unicode(30))
year = Field(Integer)
description = Field(UnicodeText)
Dziedziczenie
W przykładzie z relacjami stworzyliśmy model reżyserów. Gdybyśmy teraz chcieli jeszcze dodać aktorów to możemy stworzyć zwykły model dla aktorów i połączyć go relacją z modelem filmów. Patrząc na to z innej strony zauważymy że i aktorzy i reżyserzy to ludzie, co można objąć modelami i wykorzystać dziedziczenie. Dostaniemy wtedy model "Person" zawierający wszystkich ludzi. Modele Actor i Director dziedziczące go będą zawierały tylko ludzi określonego typu.
Skasuj plik z bazą danych i zmodyfikuj modele do postaci:from elixir import *
metadata.bind = "sqlite:///movies.sqlite"
metadata.bind.echo = True
class Person(Entity):
using_options(inheritance='multi')
name = Field(Unicode(60))
def __repr__(self):
return '<Person "%s">' % self.name
class Actor(Person):
using_options(inheritance='multi')
movies = ManyToMany('Movie')
def __repr__(self):
return '<Actor "%s">' % self.name
class Director(Person):
using_options(inheritance='multi')
movies = OneToMany('Movie')
def __repr__(self):
return '<Director "%s">' % self.name
class Genre(Entity):
name = Field(Unicode(15), primary_key=True)
movies = ManyToMany('Movie')
def __repr__(self):
return '<Genre "%s">' % self.name
class Movie(Entity):
title = Field(Unicode(30), primary_key=True)
year = Field(Integer, primary_key=True)
description = Field(UnicodeText)
director = ManyToOne('Director')
actors = ManyToMany('Actor') # new one
genres = ManyToMany('Genre')
def __repr__(self):
return '<Movie "%s" (%d)>' % (self.title, self.year)
from model import *
setup_all(True)
rscott = Director(name=u"Ridley Scott")
glucas = Director(name=u"George Lucas")
hford = Actor(name=u"Harrison Ford")
mhamill = Actor(name=u"Mark Hamill")
sweaver = Actor(name=u"Sigourney Weaver")
session.commit()
print Person.query.all()
print Actor.query.all()
[<Director "Ridley Scott">, <Director "George Lucas">, <Actor "Harrison Ford">, <Actor "Mark Hamill">, <Actor "Sigourney Weaver">]
[<Actor "Harrison Ford">, <Actor "Mark Hamill">, <Actor "Sigourney Weaver">]
Comment article