3 min read

#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
#TIL: Sqlite Row Factory
Photo by Caspar Camille Rubin / Unsplash

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:

Using a namedtuple factory with python sqlite