#TIL: Sqlite Row Factory
Saat menggunakan raw sql daripada ORM satu hal yang menurut saya tidak nyaman dalam menggunakannya adalah bagaimana hasil dari query dikembalikan. Contoh saya memiliki kode seperti ini
import sqlite3
con = sqlite3.connect("library.sqlite3")
query = "SELECT id, isbn, title FROM books"
con = con.execute(query)
rows = con.fetchall()
Kode di atas akan menghasilkan response seperti berikut
[
(1, "1234567890", "Book 1 by Author 1"),
(2, "2345678901", "Book 2 by Author 1"),
(3, "3456789012", "Book 3 by Author 1"),
(4, "4567890123", "Book 1 by Author 2"),
(5, "5678901234", "Book 2 by Author 2"),
]
Di sini yang membuat tidak nyaman adalah saat mengakses nilai kita harus mengakses melalui indeks sesuai urutan kolom dari query yang ditulis
for r in rows:
print(f"{r[0]} - {r[1]} - {r[2]}")
# indeks 0 = id
# indeks 1 = isbn
# indeks 2 = title
# Result
# 1 - 1234567890 - Book 1 by Author 1
# 2 - 2345678901 - Book 1 by Author 1
# 3 - 3456789012 - Book 1 by Author 1
# 4 - 4567890123 - Book 1 by Author 2
# 5 - 5678901234 - Book 1 by Author 2
Terlihat tidak masalah memang, tapi bayangkan jika perlu memanggil puluhan kolom.
sqlite3.Row
Cara lain untuk mengakses nilai tanpa perlu mengingat urutan kolom dari query adalah dengan sqlite3.Row
, dengan cara ini yang perlu dilakukan adalah memanggil nilai dengan key
berupa nama kolom bukan dari indeks.
import sqlite3
con = sqlite3.connect("library.sqlite3")
con.row_factory = sqlite3.Row # tambahkan ini
query = "SELECT id, isbn, title FROM books"
con = con.execute(query)
rows = con.fetchall()
Sehingga untuk memanggil nilai dari hasil query bisa dipanggil dengan nama kolom
for r in rows:
print(f"{r['id']} - {r['isbn']} - {r['title']}")
# Result
# 1 - 1234567890 - Book 1 by Author 1
# 2 - 2345678901 - Book 1 by Author 1
# 3 - 3456789012 - Book 1 by Author 1
# 4 - 4567890123 - Book 1 by Author 2
# 5 - 5678901234 - Book 1 by Author 2
Dengan cara di atas tidak perlu lagi mengingat urutan dari kolom, sangat membantu jika query memanggil banyak kolom sekaligus.
Dataclass
Jika lebih senang memperlakukan hasil query sebagai sebuah objek pada umumnya maka bisa diubah row factory menggunakan dataclass, sehingga saat dari contoh di atas saya bisa membuat objek Book
dan nantinya bisa mengakses nilai dari hasilnya seperti ini Book.title
.
Pertama ubah dulu kodenya menjadi seperti ini
import sqlite3
from dataclasses import dataclass
@dataclass
class Books:
id: int
isbn: str
title: str
# convert result row to dataclass
def converter_books(cursor, row):
fields = [column[0] for column in cursor.description]
return Books(**{k: v for k, v in zip(fields, row)})
con = sqlite3.connect("library.sqlite3")
con.row_factory = converter_books # tambahkan ini
query = "SELECT id, isbn, title FROM books"
con = con.execute(query)
rows = con.fetchall()
Sehingga untuk mengakses nilainya bisa menjadi seperti berikut
for r in rows:
print(f"{r.id} - {r.isbn} - {r.title}")
# Result
# 1 - 1234567890 - Book 1 by Author 1
# 2 - 2345678901 - Book 1 by Author 1
# 3 - 3456789012 - Book 1 by Author 1
# 4 - 4567890123 - Book 1 by Author 2
# 5 - 5678901234 - Book 1 by Author 2
Poin plusnya adalah mirip dengan sqlite3.Row
tak perlu mengingat urutan saat memanggil tapi saat membuat query pastikan urutan kolom sesuai dengan urutan kolom di dataclass. Kelebihan lainnya dengan membaca dataclass nya sendiri saya sudah bisa tahu query ini akan menghasilkan apa.
Trade Off
Seperti kata senior saya, segala keputusan di pemograman itu mengenai trade off, tinggal pilih aja bobot kekurangan yang bisa diterima dan dibandingkan dengan keuntungan yang didapat. Untuk membandingkan ketiga cara di atas saya menggunakan cProfile, setidaknya melihat waktu dan proses (simplify) dari bagaimana ketiga cara itu diproses.
Default:
11946 function calls (11905 primitive calls) in 0.157 seconds
sqlite3.Row:
11946 function calls (11905 primitive calls) in 0.165 seconds
Dataclass
52279 function calls (52234 primitive calls) in 0.231 seconds
Berdasarkan data di atas menurut saya yang paling pas dipilih adalah sqlite3.Row
secara penggunaan masih memudahkan karena yang diakses adalah nama kolom dan tidak peduli urutan dari query seperti cara default, sedangkan Dataclass lonjakan penggunaan fungsi yang dipanggil meloncat tajam dan terlihat lumayan secara signifikan perbedaan kecepatannya, tapi secara informasi dengan dataclass jauh lebih verbose.
Tes di atas dilakukan melalui laptop Thinkpad T430
CPU: Intel i5-3320M (4) @ 3.300GHz
Memory: 4993MiB / 9637MiB
OS: Ubuntu 22.04.4 LTS x86_64
Referensi: