Executing queries
Executing queries happens using transactions.
Tip
When using sqlorm, you do not interact with cursors directly. Cursors will be created for each executed statements.
Executing statements with no results
Use the transaction's execute(stmt, params)
method to execute a single non-returning statement. Parameters are optional.
The first argument can be a list of statements.
with engine as tx:
tx.execute("CREATE TABLE tasks (id integer primary key, title text, done boolean)")
tx.execute("INSERT INTO tasks (title, done) VALUES (?, ?)", ["task title", True])
The parameters format (placeholders and values passed as argument to execute()
) depends on the DBAPI driver.
Use ParametrizedStmt
to combine an sql statement and parameters:
from sqlorm import ParametrizedStmt
with engine as tx:
stmt = ParametrizedStmt("INSERT INTO tasks (title, done) VALUES (?, ?)", ["task title", True])
tx.execute(stmt)
SQL
utilities to build sql statements can also be used:
from sqlorm import SQL
with engine as tx:
tx.execute(SQL.insert("tasks", {"title": "task title", "done": True}))
Fetching from the database
Use the transaction's fetch(stmt, params)
method to fetch rows from the database.
The cursor will be automatically closed when all rows have been fetched.
It returns a ResultSet
with a few methods to handle the cursor:
fetch()
returns the next row in the cursorfirst()
returns the first row and closes the cursorall()
fetches all rows, closes the cursor and returns a listscalar()
fetches the first row and returns the value from the first columnscalars()
fetches the value from the first column of each rowsclose()
closes the cursor manually
The result set is iterable.
There are a few shortcut methods on the transaction:
fetchone()
is equivalent tofetch().first()
- ̀
fetchall()
is equivalent tofetch().all()
fetchscalar()
is equivalent tofetch().scalar()
fetchscalars()
is equivalent tofetch().scalars()
with engine as tx:
task_row = tx.fetch("SELECT * FROM tasks WHERE id = ?", [1]).first()
task_row = tx.fetchone("SELECT * FROM tasks WHERE id = ?", [1]) # same as line before
for row in tx.fetch("SELECT * FROM tasks"):
print(row)
count = tx.fetchscalar("SELECT COUNT(*) FROM tasks")
titles = tx.fetchscalars("SELECT title FROM tasks")
The loader
argument on fetch()
can be used to process each rows:
with engine as tx:
titles = tx.fetchall("SELECT title FROM tasks", loader=lambda r: r["title"])
Fetching objects
Use the model
argument of fetch()
to fetch rows as objects.
class Task:
pass
with engine as tx:
tasks = tx.fetch("SELECT * FROM tasks", model=Task)
task1 = tx.fetchone("SELECT * FROM tasks WHERE id = 1", model=Task)
Note
- The process of populating objects with data from the database is called "hydration".
- When creating objects,
__init__()
will be bypassed as well as any custom__setattr__
.
Learn more about sqlorm hydration process in the mapper section
You can update existing objects by passing an object instance to the obj
argument.
In this case, only the first row will be used to hydrate the object. The cursor is then closed.
with engine as tx:
task = Task()
tx.fetch("INSERT INTO tasks (title) VALUES ('task title') RETURNING *", obj=task)
assert task.id
assert task.title == "task title"
Models and mappers can be used to customize how objects are mapped.
Mapper
instances can also be provided as models.
Composite rows
What are composite rows ?
It is common to use joins to fetch a row with its related rows at the same time. We then need to process the row to extract the composited rows. Because of the way joining works, a single composited rows can be represented as multiple returned rows. sqlorm handles this with a custom cursor iterator that will combine multiple rows and allow you to extract hierarchical data out of your results.
To do so, sqlorm relies on column aliases. Column aliases will represent the path under which these composite
rows will be nested. Use a double underscore (__
) to separate each path segments.
Example:
SELECT
posts.id,
posts.title,
posts.posted_at
comments.id AS comments__id
comments.author AS comments__author
comments.message AS comments__message
FROM
posts
LEFT JOIN comments ON comments.post_id = posts.id
This query would return results like this:
id | title | posted_at | comments__id | comment__author | comments__message |
---|---|---|---|---|---|
1 | First post | 2024-01-01 | 1 | John | First ! |
1 | First post | 2024-01-01 | 2 | Paul | You make grammar mistakes... |
2 | Second post | 2024-01-02 |
And sqlorm will process them into the following structure:
[
{
"id": 1,
"title": "First post",
"posted_at": "2024-01-01",
"comments": [
{
"id": 1,
"author": "John",
"message": "First !"
},
{
"id": 2,
"author": "Paul",
"message": "You make grammar mistakes..."
}
]
},
{
"id": 2,
"title": "Second post",
"posted_at": "2024-01-02"
}
]
Fetching composite rows
To fetch composite rows, use fetchcomposite()
. It has a similar API to fetch()
.
with engine as tx:
rows = tx.fetchcomposite("SELECT ... FROM posts LEFT JOIN comments ...")
Using the SQL
utilities makes it easier to build these kind of queries:
with engine as tx:
stmt = SQL.select(SQL.List([
SQL.Cols(["id", "title", "posted_at"], table="posts"),
SQL.Cols(["id", "author", "message"], table="comments", prefix="comments__")
])).from_("posts").left_join("comments").on("comments.post_id = posts.id")
rows = tx.fetchcomposite(stmt)
Fetching composite objects
The model
argument can be used to return objects. The nested
argument can be used
to indicate what class to use for nested rows:
class Comment:
pass
with engine as tx:
stmt = "..."
tasks = tx.fetchcomposite(stmt, model=Task, nested={"comments": Comment})
fetchhydrated()
is an alias of fetchcomposite()
where the model argument comes first:
with engine as tx:
tasks = tx.fetchhydrated(Task, stmt)