SQLite är en populär relationsdatabas som du bäddar in i din applikation. Python kommer med officiella bindningar till SQLite. Den här artikeln undersöker varningar för att använda SQLite i Python. Det visar problem som olika versioner av länkade SQLite-bibliotek kan orsaka, hur datetime
objekt lagras inte korrekt, och hur du måste vara extra försiktig när du litar på Pythons with connection
sammanhangshanteraren för att överföra din data.
Introduktion
SQLite är ett populärt, relationsdatabassystem (DB) . Till skillnad från sina större, klientserverbaserade bröder, som MySQL, kan SQLite bäddas in i din applikation som ett bibliotek . Python stöder officiellt SQLite via bindningar (officiella dokument). Men att arbeta med dessa bindningar är inte alltid enkelt. Förutom de generiska SQLite-förbehållen som jag diskuterade tidigare, finns det flera Python-specifika frågor som vi kommer att undersöka i den här artikeln .
Versionsinkompatibilitet med implementeringsmål
Det är ganska vanligt att utvecklare skapar och testar kod på en maskin som är (väldigt) annorlunda än den där koden är utplacerad, vad gäller operativsystem (OS) och hårdvara. Detta orsakar tre typer av problem:
- Applikationen beter sig annorlunda på grund av skillnader i OS eller maskinvara . Du kan till exempel stöta på prestandaproblem när målmaskinen har mindre minne än din maskin. Eller så kan SQLite utföra vissa operationer långsammare på ett operativsystem än på andra, eftersom de underliggande lågnivå-OS-API:erna som den använder är olika.
- SQLite versionen på distributionsmålet skiljer sig från utvecklarmaskinens version . Detta kan orsaka problem i båda riktningarna, eftersom nya funktioner läggs till (och beteende ändras) över tid, se den officiella ändringsloggen. Till exempel kan en föråldrad distribuerad SQLite-version sakna funktioner som fungerade bra under utvecklingen. Dessutom kan en nyare SQLite-version i driftsättning beter sig annorlunda än en äldre version du använder på din utvecklingsmaskin, t.ex. när SQLite-teamet ändrar vissa standardvärden.
- Antingen kan Python-bindningarna för SQLite eller C-biblioteket saknas helt på distributionsmålet . Detta är en Linux -distributionsspecifikt problem . Officiella Windows- och macOS-distributioner kommer att innehålla en buntad version av SQLite C-biblioteket. På Linux är SQLite-biblioteket ett separat paket. Om du själv kompilerar Python, t.ex. eftersom du använder en Debian/Raspbian/etc. distribution som levereras med gamla funktionsversioner, Python
make
build script kommer bara att bygga Pythons SQLite-bindningar om ett installerat SQLite C-bibliotek upptäcktes under Pythons kompileringsprocess . Om du gör en sådan omkompilering av Python själv, bör du se till att det installerade SQLite C-biblioteket är nyligen . Detta är återigen inte fallet för Debian etc. när du installerar SQLite viaapt
, så du kanske också måste bygga och installera SQLite själv, tidigare att bygga Python.
För att ta reda på vilken version av SQLite C-biblioteket som används av din Python-tolk, kör det här kommandot:
python3 -c "import sqlite3; print(sqlite3.sqlite_version)"
Code language: Bash (bash)
Ersätter sqlite3.sqlite_version
med sqlite3.version
ger dig versionen av Pythons SQLite bindningar .
Uppdaterar det underliggande SQLite C-biblioteket
Om du vill dra nytta av funktioner eller buggfixar i den senaste SQLite-versionen har du tur. SQLite C-biblioteket är vanligtvis länkat under körning och kan därför ersättas utan några ändringar i din installerade Python-tolk. De konkreta stegen beror på ditt operativsystem (testat för Python 3.6+):
1) Windows: Ladda ner x86- eller x64-förkompilerade binärfiler från SQLite-nedladdningssidan och ersätt sqlite3.dll
fil som finns i DLLs
mapp för din Python-installation med den du just laddade ner.
2) Linux: från SQLite-nedladdningssidan hämta autoconf källor, extrahera arkivet och kör ./configure && make && make install
som kommer att installera biblioteket i /usr/local/lib
som standard.
Lägg sedan till raden export LD_LIBRARY_PATH=/usr/local/lib
i början av skalskriptet som startar ditt Python-skript, vilket tvingar din Python-tolk att använda det självbyggda biblioteket.
3) macOS: från min analys verkar det som att SQLite C-biblioteket är kompilerat till Python-bindningarna binär (_sqlite3.cpython-36m-darwin.so
). Om du vill byta ut den behöver du förmodligen skaffa Python-källkoden som matchar din installerade Python-installation (t.ex. 3.7.6
eller vilken version du än använder). Kompilera Python från källan med hjälp av macOS-byggskriptet. Det här skriptet inkluderar nedladdning och byggande av SQLites C-bibliotek, så se till att redigera skriptet för att referera till den senaste SQLite-versionen. Använd slutligen den kompilerade bindningsfilen (t.ex. _sqlite3.cpython-37m-darwin.so
), för att ersätta den föråldrade.
Arbeta med tidszonsmedveten datetime
objekt
De flesta Python-utvecklare använder vanligtvis datetime
objekt när du arbetar med tidsstämplar. Det finns naiva datetime
objekt som inte känner till sin tidszon och icke-naiva sådana, som är medvetna om tidszon . Det är välkänt att Pythons datetime
modulen är udda, vilket gör det svårt att ens skapa tidszonsmedvetna datetime.datetime
föremål. Till exempel anropet datetime.datetime.utcnow()
skapar en naiv objekt, vilket är kontraintuitivt för utvecklare som är nya med datetime
API:er, förväntar sig att Python ska använda UTC-tidszonen! Tredjepartsbibliotek, som python-dateutil, underlättar denna uppgift. För att skapa ett tidszonsmedvetet objekt kan du använda kod som denna:
from dateutil.tz import tzutc
import datetime
timezone_aware_dt = datetime.datetime.now(tzutc())
Code language: Python (python)
Tyvärr, den officiella Python-dokumentationen för sqlite3
modulen är missvisande när det gäller hantering av tidsstämplar. Som beskrivs här, datetime
objekt konverteras automatiskt när du använder PARSE_DECLTYPES
(och deklarerar en TIMESTAMP
kolumn). Även om detta är tekniskt korrekt, kommer konverteringen att förloras tidszonen information ! Följaktligen, om du faktiskt använder tidszon-medveten datetime.datetime
objekt måste du registrera dina egna omvandlare , som behåller tidszonsinformation, enligt följande:
def convert_timestamp_to_tzaware(timestamp: bytes) -> datetime.datetime:
# sqlite3 provides the timestamp as byte-string
return dateutil.parser.parse(timestamp.decode("utf-8"))
def convert_timestamp_to_sqlite(dt: datetime.datetime) -> str:
return dt.isoformat() # includes the timezone information at the end of the string
sqlite3.register_converter("timestamp", convert_timestamp_to_tzaware)
sqlite3.register_adapter(datetime.datetime, convert_timestamp_to_sqlite)
Code language: Python (python)
Som du kan se lagras tidsstämpeln bara som TEXT
i slutet. Det finns ingen riktig "datum" eller "datetime" datatyp i SQLite.
Transaktioner och auto-commit
Pythons sqlite3
modulen överför inte automatiskt data som ändras av dina frågor . När du utför frågor som på något sätt förändrar databasen måste du antingen utfärda en uttrycklig COMMIT
uttalande, eller så använder du anslutningen som kontexthanterare objekt, som visas i följande exempel:
with connection: # this uses the connection as context manager
# do something with it, e.g.
connection.execute("SOME QUERY")
Code language: Python (python)
När blocket ovan avslutats, sqlite3
anropar implicit connection.commit()
, men gör det bara om en transaktion pågår . DML-satser (Data Modification Language) startar automatiskt en transaktion, men frågor som involverar DROP
eller CREATE
TABLE
/ INDEX
uttalanden gör det inte, eftersom de inte räknas som DML enligt dokumentationen. Detta är kontraintuitivt, eftersom dessa uttalanden helt klart ändrar data.
Alltså, om du kör någon DROP
eller CREATE
TABLE
/ INDEX
uttalanden i kontexthanteraren, är det god praxis att explicit köra en BEGIN TRANSACTION
uttalande först , så att kontexthanteraren faktiskt anropar connection.commit()
för dig.
Hantering av 64-bitars heltal
I en tidigare artikel har jag redan diskuterat att SQLite har problem med stora heltal som är mindre än -2^63
, eller större eller lika med 2^63
. Om du försöker använda dem i frågeparametrar (med ?
symbol), Pythons sqlite3
modulen kommer att visa en OverflowError: Python int too large to convert to SQLite INTEGER
, skyddar dig från oavsiktlig dataförlust.
För att korrekt hantera mycket stora heltal måste du:
- Använd
TEXT
typ för motsvarande tabellkolumn, och - Konvertera numret till
str
redan i Python innan du använder den som en parameter. - Konvertera tillbaka strängarna till
int
i Python, närSELECT
ing data
Slutsats
Pythons officiella sqlite3
modulen är en utmärkt bindning till SQLite. Utvecklare som är nya inom SQLite måste dock förstå att det finns en skillnad mellan Python-bindningarna och det underliggande SQLite C-biblioteket. Det finns en fara som lurar i skuggorna på grund av versionsskillnader för SQLite. Dessa kan hända även om du kör samma Python-version på två olika maskiner, eftersom SQLite C-biblioteket fortfarande kan använda en annan version. Jag diskuterade också andra problem som att hantera datetime-objekt och att ständigt ändra data med hjälp av transaktioner. Jag var inte medveten om dem själv, vilket orsakade dataförlust för användare av mina applikationer, så jag hoppas att du kan undvika samma misstag som jag gjorde.