Scene 01
Declaring a relation
A relation is declared on the column that holds the key. You call relation on the column. The first argument is the target, written as package dot table dot column. Inside the same package, table dot column is enough. The invoice column customer_id points to the id of customer. foreign_key True asks for a physical foreign key. In a package, a relation gets a foreign key even when you omit the argument. Migration creates the constraint with ON UPDATE CASCADE and an index on the column.
Example & screen notes
Slide title "relation(...)". Code from the course model:
tbl.column('customer_id', size='22').relation(
'customer.id', relation_name='invoices', onDelete='raise', foreign_key=True)
Below, the DDL produced by migration:
ALTER TABLE "invc"."invc_invoice"
ADD CONSTRAINT "fk_935fad9a" FOREIGN KEY ("customer_id")
REFERENCES "invc"."invc_customer" ("id") ON UPDATE CASCADE;
CREATE INDEX "idx_3e295060" ON "invc"."invc_invoice" USING btree ("customer_id");
Scene 02
Python actions and SQL actions
A relation can say what happens when the target row is deleted. The argument onDelete is an action that asqueel runs in Python, during its own delete. The argument onDelete_sql is an action written into the foreign key, so PostgreSQL runs it. The two are separate. The DDL of the previous scene has no ON DELETE clause, because the course model declares only onDelete. Lesson 23 explains both families of actions. In this lesson, relations are used to read.
Example & screen notes
Slide title "onDelete and onDelete_sql". Table:
| Argument | Who runs it | Values |
|---|---|---|
onDelete |
asqueel, in Python | raise, cascade, setnull, ignore |
onDelete_sql |
PostgreSQL, in the foreign key | cascade, restrict, no action, setnull, setdefault |
Bottom line: "Details in lesson 23."
Scene 03
A path through a relation
A query reads a related column with a path. The path starts with an at sign and the name of the column that holds the key. Here it is at customer_id. A dot follows, and then a column of the target table. The relation is named after the key column, not after the target table. The compiler turns the path into a LEFT JOIN. Both columns of the customer use the same join, with the alias t1. One join is created per path, and every column on that path reuses it. The result column has a name built from the path. Each character that is not a letter, a digit or an underscore becomes an underscore. So the path becomes underscore customer_id underscore account_name.
Example & screen notes
Slide title "@customer_id.account_name". Code:
db.table('invc.invoice').query(
columns='$inv_number, @customer_id.account_name, @customer_id.email').sqltext
Output:
SELECT "t0"."inv_number" AS "inv_number",
"t1"."account_name" AS "_customer_id_account_name",
"t1"."email" AS "_customer_id_email"
FROM "invc"."invc_invoice" AS "t0"
LEFT JOIN "invc"."invc_customer" AS "t1" ON (("t0"."customer_id")="t1"."id")
WHERE ("t0"."__del_ts" IS NULL)
Highlight @customer_id, then LEFT JOIN, then the alias _customer_id_account_name.
Scene 04
Several hops
A path can cross several relations. Each step is an at sign and a key column of the table reached so far. The last element is a plain column. From an invoice row, the path goes to the invoice, to the customer, to the state and to the region. The compiler writes one LEFT JOIN per step. Paths work in where and order_by too. A path used only in where still adds its joins to the query.
Example & screen notes
Slide title "@a.@b.c". Code:
db.table('invc.invoice_row').query(
columns='$quantity, @invoice_id.@customer_id.@state.@region_code.name').sqltext
Output:
SELECT "t0"."quantity" AS "quantity",
"t4"."name" AS "_invoice_id__customer_id__state__region_code_name"
FROM "invc"."invc_invoice_row" AS "t0"
LEFT JOIN "invc"."invc_invoice" AS "t1" ON (("t0"."invoice_id")="t1"."id")
LEFT JOIN "invc"."invc_customer" AS "t2" ON (("t1"."customer_id")="t2"."id")
LEFT JOIN "invc"."invc_state" AS "t3" ON (("t2"."state")="t3"."code")
LEFT JOIN "invc"."invc_region" AS "t4" ON (("t3"."region_code")="t4"."code")
WHERE ("t0"."__del_ts" IS NULL)
Scene 05
The inverse name
Every relation also exists in the other direction. relation_name gives the name of that inverse side. The invoice_row relation declares rows. So, from an invoice, at rows leads to its rows. Without relation_name, the inverse side gets a name made from the package, the table and the column, for example invc_customer_profile_customer_id. The inverse side reaches many rows. A query cannot project it as a column. at rows alone raises ValueError. at rows dot quantity raises UnsupportedFeatureError. asqueel never multiplies the rows of a query through a join towards many. The inverse side is used through aggregation, which lesson 06 covers.
Example & screen notes
Slide title "relation_name". Two lines:
# invoice_row
tbl.column('invoice_id', size='22').relation(
'invoice.id', relation_name='rows', onDelete='cascade', foreign_key=True)
Table:
Expression on invc.invoice |
Result |
|---|---|
@rows |
ValueError: Unknown column invc.invoice.@rows |
@rows.quantity |
UnsupportedFeatureError: ... a path through the many relation @rows is not available in this release |
@rows.aggr(count() AS n) |
one column, lesson 06 |
Scene 06
one_one
one_one True states that each target row has at most one row on the other side. A customer has at most one profile. With this flag, the inverse side is a path towards one. From the customer, at profile dot website is a LEFT JOIN, like a forward path. The flag does not create a unique constraint. If the data holds two profiles for one customer, the join returns two rows. Declare a unique column or a constraint when the database must enforce it.
Example & screen notes
Slide title "one_one=True". Code, an addition to the course model:
profile = pkg.table('customer_profile', pkey='id')
profile.options().sys_fields()
profile.column('customer_id', size='22').relation(
'invc.customer.id', relation_name='profile', one_one=True, onDelete='cascade')
profile.column('website')
db.table('invc.customer').query(columns='$account_name, @profile.website').sqltext
SELECT "t0"."account_name" AS "account_name",
"t1"."website" AS "_profile_website"
FROM "invc"."invc_customer" AS "t0"
LEFT JOIN "invc"."invc_customer_profile" AS "t1" ON (("t0"."id")="t1"."customer_id")
WHERE ("t0"."__del_ts" IS NULL) AND ("t0"."__is_draft" IS NOT TRUE)
Scene 07
weak_relation
weak_relation declares a relation that only navigates. It never creates a foreign key. It can sit on a physical column or on a virtual column. Here the formula last_invoice_id selects the id of the latest invoice of the customer. Lesson 08 explains formulas. The weak relation makes the formula a key. A query can then read at last_invoice_id dot inv_number. The join compares the result of the formula with the invoice id. relation on a virtual column behaves the same way. It refuses foreign_key True.
Example & screen notes
Slide title "weak_relation: navigation without a foreign key". Code, an addition copied from examples/test_invoice_pg/invc.py:
customer.formulaColumn('last_invoice_id', select=dict(
table='invc.invoice', columns='$id', where='$customer_id=#THIS.id',
order_by='$date DESC', limit=1), dtype='T').weak_relation(
'invoice.id', relation_name='last_of_customer')
db.table('invc.customer').query(
columns='$account_name, @last_invoice_id.inv_number, @last_invoice_id.date')
Output, the join only:
LEFT JOIN "invc"."invc_invoice" AS "t1" ON ((( ( SELECT "t0_t0"."id" AS "id"
FROM "invc"."invc_invoice" AS "t0_t0"
WHERE ("t0_t0"."customer_id"="t0"."id")
ORDER BY "t0_t0"."date" DESC
LIMIT 1 ) ))="t1"."id")
Scene 08
An alias inherits the relation
An alias column reads a column through a relation path. Lesson 08 covers aliases in full. When the target of the alias carries a relation, the alias inherits it. On the invoice row, the alias customer_id reads the customer_id of the invoice. That column points to the customer. So the alias points to the customer too. The path at customer_id dot account_name starts from the row and reaches the customer through the invoice. The alias gets only the target of the relation. It gets no foreign key, no inverse name and no delete action.
Example & screen notes
Slide title "aliasColumn keeps the relation". Code, an addition copied from examples/test_invoice_pg/invc.py:
row = pkg.table('invoice_row', pkey='id')
row.aliasColumn('customer_id', relation_path='@invoice_id.customer_id')
db.table('invc.invoice_row').query(columns='$quantity, @customer_id.account_name')
SELECT "t0"."quantity" AS "quantity",
"t1"."account_name" AS "_customer_id_account_name"
FROM "invc"."invc_invoice_row" AS "t0"
LEFT JOIN "invc"."invc_invoice" AS "t2" ON (("t0"."invoice_id")="t2"."id")
LEFT JOIN "invc"."invc_customer" AS "t1" ON (("t2"."customer_id")="t1"."id")
Scene 09
Recap
A path names the key column and becomes one LEFT JOIN per path. The inverse side has the name given by relation_name, and towards many it is used only through aggregation. one_one, weak relations and aliases change navigation only. The next lesson covers aggregations over the inverse side.
Example & screen notes
Three bullets:
@<key column>.<column>reads a related column through one LEFT JOIN per pathrelation_namenames the inverse side; towards many it needs aggregationone_one,weak_relationand an alias column change how a relation is navigated, not the database