SQLite to nie jest zły wybór

Ten post został napisany ponad 2 lata temu, do wszystkich porad technologicznych w nim zawartych lepiej będzie podejść z dużą rezerwą, bo bardzo możliwe że tego rodzaju informacje są już nieaktualne.

Opublikowano: 02.07.2020

Ostatnia modyfikacja: 07.02.2024

orm

programowanie

python

sqlite

Powszechnie uważa się, że SQLite to jest baza która nie nadaje się do użycia w aplikacji webowej, w każdym razie nie na etapie produkcji. Rzeczywiście, w wielu przypadkach (większości?) się nie sprawdzi z powodu ograniczeń jakie narzuca w kontekście współbieżnego dostępu, ale są pewne warunki, w których to nie ma żadnego znaczenia i wtedy SQLite sprawdza się doskonale.

Zielone winko

W skrócie chodzi o to, że jako silnik wbudowany nie jest ona przystosowana do używania w trybie współdzielonym, a przynajmniej nie w pełni. Chodzi o to, że nie ma żadnego sposobu, by bezpiecznie przeprowadzić jednoczesny zapis przez więcej niż jeden proces/wątek. Wydawałoby się, że to dyskwalifikuje z wykorzystania jako zaplecza w aplikacji webowej, ale czy na pewno? Czy na pewno nie ma takich aplikacji, w których dokonywany jest niemal wyłącznie odczyt, a zapis jest albo serializowany, albo w ogóle jest tylko jeden proces który coś zapisuje?

Nie ma?

Ależ oczywiście że są. I wcale nie są jakąś rzadkością. Po prostu większość wzrusza na nie ramionami.

Weźmy na przykład taki blog osobisty. Jest jeden edytor, który dokonuje zmian w zawartości bazy danych, wszyscy pozostali użytkownicy mają dostęp tylko do odczytu. Współbieżny odczyt nie stanowi problemu, a przy niewielkiej modyfikacji parametrów połączenia również współbieżny odczyt przez wiele procesów/wątków i zapis przez jeden proces/wątek (domyślnie zapis blokuje całą tabelę na wyłączność). Czy są jeszcze jakieś inne przypadki? Pewnie są, trzeba się tylko rozejrzeć.

Tym, co z zapyziałej bazy trybu developerskiego zrobi produkcyjne zaplecze aplikacji webowej są polecenia PRAGMA. Kompletny opis wszystkich poleceń PRAGMA znajduje się na stronie dokumentacji SQLite, ja zaś skupię się na tych kilku interesujących w kontekście aplikacji webowej.

Z tymi ustawieniami SQLite jest wyjątkowo wydajne, ma wyższy stopień zgodności ze standardem SQL (co ułatwia życie w przypadku używania ORM) - oraz może być jednocześnie używane w trybie zapisu i odczytu.

Wsparcie w ORM

Najlepiej wygląda to w Peewee - jest to dokładnie omówione, wraz z opisem zalecanych opcji. Autor Peewee w wielu artykułach na swoim blogu opisuje różne zagadnienia związane ze SQLite, w tym również funkcje, o których się głośno nie mówi i widać że się tym dość mocno interesuje. Spoglądając na inicjalizator obiektu bazy danych widać, że wsparcie dla pragm jest pierwszorzędne - podaje się je jako słownik.

db = SqliteDatabase(
    'my_app.db',
    pragmas={
        'journal_mode': 'wal',
        'cache_size': -1 * 64000,  # 64MB
        'foreign_keys': 1,
        'ignore_check_constraints': 0,
        'synchronous': 0
    }
)

W Pony zastosowano rozwiązanie close to the metal. Ponieważ faktycznie pragmy podaje się przy każdym otwieraniu bazy danych SQLite jako zwykłe polecenia (tak jak SQL), to tak też je trzeba do bazy przesłać wpinając się w momencie gdy połączenie zostanie otwarte i to właśnie przedstawia przykład w dokumentacji.

db = Database()

# entities declaration

@db.on_connect(provider='sqlite')
def sqlite_pragmas(db, connection):
    cursor = connection.cursor()
    cursor.execute('PRAGMA journal_mode = WAL')
    cursor.execute('PRAGMA ignore_check_constraints = 0')
    cursor.execute('PRAGMA cache_size = -64000')

db.bind(**options)
db.generate_mapping(create_tables=True)

Powyższy przykład pomija ustawienie pragmy foreign_keys ponieważ od dłuższego czasu jest ona ustawiana automatycznie przez Pony.

W podobny sposób można to wykonać w SQLAlchemy, tutaj używając zdarzenia engine_connect.

from sqlalchemy import event

@event.listens_for(MyEngine, 'engine_connect')
def engine_connected(conn, branch):
    conn.execute('PRAGMA journal_mode = WAL')
    conn.execute('PRAGMA foreign_keys = 1')
    conn.execute('PRAGMA ignore_check_constraints = 0')
    conn.execute('PRAGMA cache_size = -64000')