Database¶
Model comes with built-in database layers for SQLite, MariaDB, and MySQL, which you can use directly to read and modify data, run SQL, manage tables, and work with database connections without defining a model.
Connect to a database¶
Database objects connect when they are created.
SQLite¶
With no arguments, SQLite creates an in-memory database. Pass a file path if you want the data to persist:
from model.database import SQLiteDatabase
db = SQLiteDatabase()
file_db = SQLiteDatabase("./data/app.db")
An in-memory database belongs to one connection. Another thread gets a separate, empty database, so use a file if multiple threads need to share the same data.
You can also pass timeout, check_same_thread, and cached_statements through other_config:
These options behave the same as they do with sqlite3.connect.
MariaDB and MySQL¶
import os
from model.database import MySQLDatabase
db = MySQLDatabase(
host="127.0.0.1",
port=3306,
user="app",
password=os.environ["DB_PASSWORD"],
database="world",
)
The main connection options are:
hostportuserpassworddatabaseother_config
Use other_config for additional PyMySQL settings such as timeouts, charset, TLS, client flags, or a Unix socket.
Unknown options raise DatabaseError.
MySQL connection pool¶
Use MySQLDatabasePool when concurrent work should share a limited number of connections:
from model.database import MySQLDatabasePool
db = MySQLDatabasePool(
pool_size=8,
pool_timeout=5,
host="127.0.0.1",
user="app",
password="secret",
database="world",
)
pool_size must be between 1 and 32.
pool_timeout controls how long an operation waits for a free connection. Set it to 0 to fail immediately when none is available.
Each query or built-in database method borrows one connection and returns it when finished.
The pool accepts the normal MySQL connection options, plus optionally fallback_servers: an ordered list of connection-option dictionaries containing fallback values such as an alternate host or port.
Read and change data¶
The examples below use SQLite's ? placeholders.
With MariaDB and MySQL, use %s instead.
Find rows¶
Use find_one() when you want one matching row:
You do not need to include WHERE.
Use find_all() for multiple rows:
cities = db.find_all(
"city",
"CountryCode = ? ORDER BY Population DESC",
["ALB"],
columns=["Name", "Population"],
)
You can also add a LIMIT clause to limit the total results returned.
Use count() when you only need the number of matching rows:
When find_one() could match several rows, add ORDER BY if you need a predictable result.
Insert rows¶
Use insert() to add one row:
result = db.insert(
"city",
{
"Name": "Saranda",
"CountryCode": "ALB",
"Population": 50_000,
},
)
print(result.rowcount)
print(result.lastrowid)
It returns an InsertResult.
Call insert() with only the table name to insert a row using database defaults and any auto-incrementing key.
Use insert_many() to add several rows with one statement:
rows = [
{
"Name": "Tirana",
"CountryCode": "ALB",
"Population": 418_495,
},
{
"Name": "Saranda",
"CountryCode": "ALB",
"Population": 50_000,
},
]
result = db.insert_many("city", rows)
The list must not be empty, and every dictionary must have the same keys in the same order.
For insert_many(), lastrowid is only the ID reported for the statement. It does not identify every inserted row.
Update rows¶
Use update() to change matching rows:
It returns the number of affected rows.
Insert or update¶
Use insert_or_update() when you want to insert a row if it does not exist, or update it when a matching unique value already exists:
result = db.insert_or_update(
"user",
{
"email": "ada@example.com",
"display_name": "Ada",
},
skip_update_on_columns=["email"],
)
print(result.is_insert)
print(result.is_update)
print(result.lastrowid)
It returns an InsertOrUpdateResult.
Delete rows¶
Use delete() to remove matching rows:
It returns the number of affected rows.
Run SQL directly¶
Use query() when you need SQL that does not fit the built-in methods, such as joins, expressions, aliases, or more complex statements.
with db.query(
"SELECT Name, Population FROM city WHERE CountryCode = ?",
["ALB"],
) as cursor:
cities = cursor.fetchall()
Keep reads and other cursor work inside the with block. The cursor closes when the block ends.
When using a connection pool, its connection is also returned to the pool.
Common cursor features include:
fetchone()fetchall()rowcountlastrowid
Returned rows¶
Rows support both dictionary-style and attribute access:
with db.query(
"SELECT Name, CountryCode FROM city LIMIT 1"
) as cursor:
city = cursor.fetchone()
if city is not None:
print(city["Name"])
print(city.Name)
You can also use get():
get() is case-insensitive.
Use dict(row) when you want a regular dictionary.
Transactions¶
Database objects use autocommit by default.
A query() block controls how long its cursor stays open. It does not automatically start, commit, or roll back a transaction.
The most portable approach is to keep the entire transaction on one cursor:
with db.query("BEGIN") as cursor:
try:
cursor.execute(
"UPDATE account SET balance = balance - ? WHERE id = ?",
[20, 1],
)
cursor.execute(
"UPDATE account SET balance = balance + ? WHERE id = ?",
[20, 2],
)
except BaseException:
cursor.execute("ROLLBACK")
raise
else:
cursor.execute("COMMIT")
Use %s placeholders with MySQL.
Manage tables and columns¶
Use these methods to inspect and change tables, including migrations inside pre_sync and post_sync model hooks. See Sync and migrations for more.
Check whether a table exists:
Check whether a column exists:
You can optionally check its type:
Column names are compared without caring about letter case.
Type checks also ignore case and match the beginning of the type, so CHAR matches CHAR(3).
Rename a column:
Remove a column:
Rename a table:
Remove a table:
When SQLite cannot directly remove a constrained column, Model tries to rebuild the table while preserving the rest of its structure.
If that cannot be done safely, it raises DatabaseError.