Blog JSystems - uwalniamy wiedzę!

Szukaj
Framework Flask i SQL Alchemy

SQLAlchemy pozwala na translację pomiędzy strukturą relacyjną w relacyjnych bazach danych, a strukturą obiektową i odwrotnie. Jest to zdecydowanie wygodniejsze rozwiązanie niż używanie takich bibliotek jak cx_Oracle czy psycopg2. Aby rozpocząć pracę z SQLAlchemy musimy zainstalować niezbędny pakiet :

pip install flask-sqlalchemy

Poza samym modułem potrzebujesz też lokalnej bazy PostgreSQL jeśli chcesz przetestować mój kod. Po zainstalowaniu PostgreSQL trzeba jeszcze utworzyć bazę danych i użytkownika poniższymi komendami z poziomu PGAdmina:

create database demo;
create user demo with password 'demo';

Łączenie z bazą i pierwsza encja

Aby rozpocząć pracę z SQLAlchemy trzeba najpierw zainicjalizować podając jej właściwy kontekst:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://demo:demo@localhost/demo'
db = SQLAlchemy(app)

Obiekt „db” to obiekt który będziemy wykorzystywać odwołując się do bazy. Przyszedł czas na obiektową reprezentację danych relacyjnych. Dla każdej tabelki/widoku tworzymy klasę dziedziczącą po klasie db.Model:

class Product(db.Model):
    __tablename__ = "products"
    productId = db.Column(db.Integer, name="product_id", primary_key=True)
    productName = db.Column(db.String, name="product_name", unique=True)
    productDescription = db.Column(db.String, name="product_description", nullable=True)
    productPrice = db.Column(db.Numeric, name="product_price", index=True)

W powyższym przykładzie użyłem kilku różnorakich argumentów by zaprezentować co ciekawsze możliwości. Nieco enigmatyczne pole „__tablename__” określa nazwę tabeli jeśli jest inna niż klasa - przydatne gdy w Pythonie używasz camelCase’a a w bazach takiego_nazewnictwa. Każde pole tej klasy jest obiektem klasy Column której argumenty inicjalizacyjne wymieniam poniżej:

  • Pierwszy argument (np. db.Integer) określa typ danych zawartych w kolumnie. Odpowiednio
    • Integer - liczby całkowite
    • String - tekst
    • Numeric - liczby zmiennoprzecinkowe
  • name - określa nazwę kolumny. Jest to parametr opcjonalny, jeśli go nie podasz to SQLAlchemy będzie szukał/tworzył kolumnę o nazwie takiej samej jak nazwa pola.
  • primary_key - określa że dana kolumna jest kluczem głównym
  • unique - określa że wartości w kolumnie muszą być unikalne. Wiąże się to z założeniem odpowiedniej reguły po stronie bazy danych.
  • nullable - określa czy wstawiając wiersze do tabeli dana kolumna może pozostać nieuzupełniona. Uwaga! Jeśli nie określisz jawnie, kolumna przy tworzeniu przez SQLAlchemy otrzyma własność NOT NULL!
  • index - określa czy na daną kolumnę przy tworzeniu ma zostać nałożony indeks. Protip: jeśli zamierzasz wyszukiwać dane na podstawie wartości zawartej w tej kolumnie, bądź pobierać dane tylko z niej - koniecznie użyj tej funkcjonalności by poprawić wydajność zapytań

Jeśli chciałbyś aby tabelki utworzyły się same, wywołaj jednokrotnie metodę:

db.create_all()

Po jej uruchomieniu w bazie danych powstaną tabelki dla wszystkich klas dziedziczących po db.Model. Skutek po stronie PostgreSQL:

116.png (262×196)

Jeśli jest jakiś problem z kontaktem z bazą, ujawni się na tym etapie.

117.jpg (217×163)

Tabelka została utworzona przez SQLAlchemy automagicznie. Jeśli nie chcesz tworzyć tabel tylko korzystać z istniejących tabel, zwyczajnie nie wywołuj metody create_all(). Gdyby przy zapytaniu okazało się że tabelka jednak nie istnieje, otrzymamy wyjątek. Istnieje też metoda db.drop_all(), a jej nazwa chyba wyjaśnia wszystko ;). Na potrzeby dalszych działań wzbogaciłem moją klasę o dwie dodatkowe metody i w całości wygląda ona teraz tak:

class Product(db.Model):
    __tablename__ = "products"
    productId = db.Column(db.Integer, name="product_id", primary_key=True)
    productName = db.Column(db.String, name="product_name", unique=True)
    productDescription = db.Column(db.String, name="product_description", nullable=True)
    productPrice = db.Column(db.Numeric, name="product_price", index=True)

    def __init__(self, pn, pd, pp):
        self.productName=pn
        self.productDescription=pd
        self.productPrice=pp

    def __str__(self):
        return f'productId={self.productId}, productName={self.productName}, productDescription={self.productDescription}, productPrice={self.productPrice}'

Dodawanie danych do tabeli

Aby dodać dane do bazy należy w pierwszej kolejności utworzyć obiekty encyjne - tj. obiekty które mamy zamiar utralać. W poniższym przykładowym kodzie tworzę 3 trakie obiekty, a następnie przekazuję je jako argument do metody add. Jest to zapis transakcyjny, toteż aby zmiany w bazie były widoczne trzeba zatwierdzić transakcję, co czynimy wywołując metodę commit():

def loadSomeData():
    p1 = Product('Bulbulator', 'Robi bul bul', 100)
    p2 = Product('Półoś do Jelcza', '3ma koło', 80)
    p3 = Product('Wahacz to taczki', 'Do taczek wyścigowych', 500)
    db.session.add(p1)
    db.session.add(p2)
    db.session.add(p3)
    db.session.commit()

Całość opakowałem w funkcję by móc osobno wywołać proces jednorazowo, co też po deklaracji uczyniłem. Tobie proponuję zrobić to samo, zanim przejdziemy do dalszej części.

Pobieranie i filtrowanie danych

Przypomnij sobie deklarację klasy Product. Był tam taki fragment:

class Product(db.Model):

oznaczający że nasza klasa dziedziczy po klasie Model. Dzięki temu dziedziczeniu użytkujemy kilka ciekawych możliwości. Między innymi nasze obiekty będą posiadały metody umożliwiające pobieranie danych z bazy:

def getAllProducts():
    return Product.query.all()

Aby pobierać dane z bazy należy zatem po nazwie klasy podać „query” i metodę zwracającą dane. W powyższym przykładzie pokazuję pobieranie wszystkich danych, stosuję zatem metodę all(). Teraz zastosujemy filtrowanie:

def getProductsPriceOver(price):
    return Product.query.filter(Product.productPrice>price).all()

Tym razem pojawia nam się jeszcze metoda filter. Przyjrzyjmy się jej zawartości. Jest nazwa klasy, nazwa pola i warunek „>price”, gdzie price to argument metody. Funkcja ta zwróci wszystkie produkty o cenie wyższej niż podana przez argument. Gdybyś zechciał dodać kolejne warunki, robisz to w ten sposób:

def getProductsPriceOver(price):
    return Product.query.filter(Product.productPrice>price).filter(1==1).all()

Czyli po kropce stosujesz kolejne wywołania metody filter. Cały czas mówimy o zwracaniu listy elementów. Co jednak jeśli zechcemy zrobić funkcję zwracającą zawsze jeden obiekt. Nie ma sensu opakowywać go w listę poprzez metodę all(). W sytuacji gdy wiemy że zawsze będzie zwracany dokładnie jeden obiekt (bo filtrujemy z użyciem unikalnej kolumny) możemy wykorzystać metodę „first()”:

def getOneProductById(productId):
    return Product.query.filter(Product.productId==productId).first()

Sortowanie wyniku

Do sortowania wyniku stosujemy metodę „order_by”. Umożliwia ona sortowanie rosnące i malejące. Jeśli chcesz posortować dane rosnąco robisz to w ten sposób:

def getAllProductsOrdered():
    return Product.query.order_by(Product.productPrice).all()

Należy podać nazwę pola po którym dane mają być sortowane. Gdybyś wolał sortowanie malejące:

def getAllProductsOrdered():
    return Product.query.order_by(Product.productPrice.desc()).all()

Filtracja i sortowanie w jednym

Metody filter i order_by można też stosować jednocześnie, należy jedynie pamiętać o właściwej kolejności:

def getProductsPriceOver(p):
    return Product.query.filter(Product.productPrice>p).order_by(Product.productPrice).all()

Zmiana istniejących w bazie danych

Ciekawostka - jaką funkcją aktualizujemy dane? Otóż jest to ta sama funkcja której używaliśmy do ich dodawania…

118.jpg (320×320)

def changePrice(product,newPrice):
    product.productPrice=newPrice
    db.session.add(product)    db.session.commit()

Skąd zatem SQLAlchemy wie czy chcemy stworzyć nowy wpis w bazie czy zaktualizować istniejący? Na podstawie wypełnionego albo nie id obiektu. Oznaczyliśmy na etapie deklaracji klasy która kolumna jest kluczem głównym:

productId = db.Column(db.Integer, name="product_id", primary_key=True)

Zatem jeśli chcesz zaktualizować obiekt, uzupełnij jego ID. Jeśli chcesz dodać nowy, podajesz go bez ID. Jeśli zmienisz id w obiekcie i wywołasz na nim zapis, dokonana zostanie aktualizacja ID a nie dodanie nowego obiektu.

Kasowanie danych

Tu nie ma nic zaskakującego. Jak zapewne po zapoznaniu się z poprzednimi przykładami można się domyślić, istnieje metoda delete umożliwiająca nam takie działanie:

def deleteProduct(product):
    db.session.delete(product)
    db.session.commit()

SQLAlchemy rozpoznaje obiekt do skasowania na podstawie wartości w kluczu głównym, podobnie jak w przypadku aktualizacji.

Podglądanie generowanego SQLa

Zdarzają się sytuację że dostajemy od SQLAlchemy jakieś dziwne wyjątki i nie możemy zdiagnozować źródła problemu. W takich sytuacjach bardzo pomaga możliwość zweryfikowania generowanego przez SQLAlchemy SQLa. W tym celu zamiast od razu wywoływać metodę all(), odbierzemy najpierw obiekt zapytania. Jego reprezentacja po wydrukowaniu pokazuje właśnie wygenerowanego SQLa:

def showMeSQL(price):
    q=Product.query.filter(Product.productPrice>price)
    print(q)
    return q.all()

showMeSQL(40)

Zapytanie które otrzymałem:

SELECT products.product_id AS products_product_id, products.product_name AS products_product_name, products.product_description AS products_product_description, products.product_price AS products_product_price
FROM products
WHERE products.product_price > %(product_price_1)
Szkolenie Python od podstaw, przez analizę danych, do machine learning w JSystems

Ten kurs daje Ci solidne podstawy Pythona. Jeśli chcesz pójść dalej i pod okiem trenera dojść od pierwszej linijki kodu do własnego modelu uczenia maszynowego, sprawdź nasze pięciodniowe szkolenie Python od podstaw, przez analizę danych, do machine learning. Pierwsze trzy dni to Python w praktyce: typy i kolekcje, instrukcje warunkowe i pętle, funkcje, moduły i pakiety, pliki tekstowe, usługi sieciowe oraz praca z bazą PostgreSQL. Czwarty dzień to analiza danych w pandas (wczytywanie CSV, TSV i XLS, filtrowanie, agregacje, statystyki) i wizualizacja w Matplotlib, Seaborn oraz Plotly. Piąty dzień to uczenie maszynowe w scikit-learn: regresja liniowa i logistyczna, drzewa decyzyjne, las losowy, XGBoost, walidacja predykcji, dobór hiperparametrów i przeciwdziałanie przeuczeniu. Szkolenie ma terminy gwarantowane, czyli odbywa się niezależnie od wielkości grupy.

Szkolenie Python od podstaw, przez analizę danych, do machine learning

To szkolenie może być dofinansowane dla Ciebie z KFS lub BUR.

★★★★★Średnia ocena naszych szkoleń w Google: 5/5

Najczęściej zadawane pytania

Co daje SQLAlchemy w aplikacji Flask?
SQLAlchemy to biblioteka mapowania obiektowo-relacyjnego, która tłumaczy między strukturą relacyjną w bazie a strukturą obiektową w kodzie i odwrotnie. Jest wygodniejsza od pisania zapytań wprost przez sterowniki takie jak psycopg2, bo na tabelach operujemy jak na klasach i obiektach Pythona.
Jak zdefiniować encję, czyli klasę mapowaną na tabelę?
Tworzymy klasę dziedziczącą po db.Model, w której pola są obiektami klasy Column. Pierwszy argument Column to typ danych (na przykład Integer, String, Numeric), a dalsze argumenty ustalają nazwę kolumny, klucz główny, unikalność, indeks czy dopuszczalność wartości pustej.
Czy trzeba samodzielnie tworzyć tabele w bazie?
Nie zawsze. Jednokrotne wywołanie db.create_all() tworzy tabele dla wszystkich klas dziedziczących po db.Model. Jeśli chcemy korzystać z już istniejących tabel, po prostu nie wołamy tej metody. Istnieje też db.drop_all(), która tabele usuwa.
Jak dodać nowe rekordy przez SQLAlchemy?
Tworzymy obiekty encyjne, przekazujemy je do db.session.add, a na końcu zatwierdzamy transakcję metodą db.session.commit(). Zapis jest transakcyjny, więc bez commit zmiany nie będą widoczne w bazie danych.
Jak pobierać i filtrować dane w SQLAlchemy?
Po nazwie klasy używamy query oraz metod zwracających dane: all() zwraca listę wszystkich rekordów, filter nakłada warunki (można je łączyć po kropce), a first() zwraca jeden obiekt, gdy filtrujemy po kolumnie unikalnej. Sortowanie realizuje metoda order_by.
Uwaga, kolumna domyślnie jest NOT NULL, jak to zmienić?
Jeśli tworząc kolumnę przez SQLAlchemy nie ustawisz jawnie parametru nullable, kolumna otrzyma własność NOT NULL. Aby pozwolić na wartości puste przy wstawianiu wierszy, trzeba jawnie ustawić nullable na wartość prawda dla danej kolumny.

Komentarze (0)

Musisz być zalogowany by móc dodać komentarz. Zaloguj się przez Google

Brak komentarzy...