Skillversitas

Best Practices

After nineteen chapters of mechanisms, the discipline that separates a maintainable, fast data layer from a tangle of ad-hoc queries is a set of rules about where the logic lives and how the schema, the model, and the query relate. The practices are not new ideas - they are the decision criteria for the tools the tutorial built: the model is the schema, the query is inspectable, the write is atomic, and the performance is the sum of a few recurring habits. This chapter collects the rules, each tied to the chapter that introduced the mechanism it governs, because the value of a practice is the mechanism that makes it possible and the failure it prevents.

Rule 1: The Model Is the Schema - Keep Them in Lockstep

The model and the database table are two descriptions of the same thing, and the rule is that they never diverge: a field on the model is a column in the table, a change to one is a change to the other, and the migration is the mechanism that moves them together (the migrations chapter's up and the model field in the same change). The divergence is the bug - a column the model does not describe (a query that cannot see it), a field the table lacks (a query that fails at runtime), a constraint the model does not enforce (a validation that is in the code but not the schema, and therefore skippable). The practices that keep them aligned: the field's arguments are the column's constraints (null, unique, the default), the Meta.indexes are the indexes the queries need (the performance chapter), and the _meta introspection (get_table_sql) is the check that the model produces the schema you think it does. The model is the single source of truth for the schema's shape, and the migration is how that truth reaches the database - the pair is the discipline, and a schema change that is not a model change plus a migration is a change that will desynchronize.

Rule 2: The Query Is Inspectable - Print It, Explain It

The query object renders its SQL, and the habit is to use that: print(q) before the execute() that matters, the explain() for the query that is slow or wrong. The inspectability is the control the ORM gives - the expression language (the query-expressions chapter) builds the SQL, the SQL is visible, and the fix is a reaction to what is visible rather than a guess. The practices: the where is always present on a query-level update/delete (the CRUD chapter's rule, because the unconditioned one touches the table), the select projects the columns the caller uses (the performance chapter's over-selection fix), the get is used only when exactly one row is expected (its DoesNotExist/MultipleResults contract is the error the caller handles), and the join is on the column the explain confirms (the joins chapter's ON inspection). The rule is not "check every query" - it is that the check is cheap and available, so the query that carries the app's data is the one that has been seen, and the slow or wrong query is explainable before it is a production incident.

Rule 3: The Write Is Atomic and the Intent Is Explicit

The multi-statement write is a transaction (the transactions chapter's atomic block), so a failure leaves the database as it was, and the single-statement writes are chosen for their intent: create for the row with an instance back, the on_conflict upsert for the shared row (the bulk chapter), the column-arithmetic update for the counter (the race-free increment), the recursive delete for the deliberate cascade (the one-to-many chapter's on_delete decision). The rule is that the write's shape states its intent - a save is the instance you changed, an update() is the condition over the rows, an upsert is the insert-or-update on the key - and the shape that does not state the intent is the one that surprises: the read-then-write with the race, the delete without the where, the cascade that was not decided. The signals chapter's hooks are the extension - the validation that must run on every write of the model, attached to the model, raised to cancel - and the rule they obey is the same: the write's invariants are enforced at the write, in one place, not repeated (and skipped) at the call sites. The data's fate - what a delete takes, what a conflict does, what a failure undoes - is stated in the write's shape, and the reader of the code sees the intent without having to trace the failure paths.

Rule 4: The Performance Habits Are the Design, Not the Tuning

The performance chapter's four problems - the N+1, the missing index, the over-selection, the unpaginated list - are designed out, not tuned away, and the design decisions are the ones made when the query is written, not when it is slow. The practices: the loop that touches a relationship prefetches or joins (the decision made at the loop, not discovered at the load test), the filter and the sort are indexed (the Meta.indexes declared with the model, the migration adding them to the live table), the select projects the columns used, and the list that is not bounded is paged (the keyset form for the large table). The pooling chapter's rules are the operational layer - the pool for the server, sized below the database's cap, the transaction short so the pool slot is not held across the slow work - and they are the same discipline at the connection level. The through-line is the one the whole tutorial holds: the query is inspectable, so the performance is diagnosable, and the habit (the query count per request, the explain of the slow one) is what keeps the designed-out state as the app grows. Performance as design is the rule - the shape of the query, the index, the projection, and the page are decided when the query is written, because they are cheaper then and the explain is the check that the decision held.

Rule 5: The Data Layer Is One Place - database, models, migrations

The installation chapter's project shape is the structural rule: database.py owns the connection (built from the environment, the pool or the proxy for the server), models.py owns the models (the fields, the relationships, the Meta), and migrations/ owns the schema's history - the data layer is a single, inspectable unit, and a new developer reads the data model in one file. The violations are the connection constructed in a module that needed it (the backend is no longer a config change), the model defined where the query is (the schema is no longer in one place), and the schema change applied by hand to the database (the history is no longer the record). The practices: the models import the one db, the from_url in the one function selects the backend, the migration is the only thing that changes the live schema, and the test swaps the in-memory database in through the proxy (the testing chapter) so the same models run against the test target. The rule is the same as every layer's - one place owns the thing, and the rest of the code depends on the interface, not the construction - and it is what makes the data layer a component the app is built on, instead of a detail scattered through the modules that use it.

The Review Checklist

  1. Do the model and the schema agree? Every field is a column, every constraint is declared, the Meta.indexes are the queries' needs, and the change is a model field plus a migration - not a hand-applied ALTER.
  2. Is the load-bearing query inspected? The where is present on the update/delete, the select projects the columns used, the get is only where one row is expected, and the join's ON is the column the explain confirms.
  3. Does each write state its intent? The atomic block around the multi-statement operation, the upsert on the shared row, the column arithmetic on the counter, the recursive on the deliberate cascade - and the invariant enforced at the write (the hook or the check), not at the call site.
  4. Are the performance habits in the design? The loop prefetches or joins, the filter and sort are indexed, the list is paged, the pool is sized and the transaction short - decided when the query was written, checked by the query count and the explain.
  5. Is the data layer one place? The connection in database.py (the pool or proxy), the models in models.py, the history in migrations/ - and the test swaps the database in through the proxy, so the shape holds in the test too.

Summary

The practices are the decision criteria for the mechanisms, and each is a lock between two things that must not diverge. The model and the schema are one description, moved together by the migration (Rule 1). The query and its SQL are one object, and the inspectability - the print, the explain, the always-present where, the projection - is the control (Rule 2). The write and its intent are one statement or one atomic block, with the upsert, the arithmetic counter, and the deliberate cascade stating what the data's fate is (Rule 3). The performance and the design are one decision, made when the query is written - the prefetch or join, the index, the projection, the page, the short transaction - and kept by the query count and the explain (Rule 4). And the data layer and the app are one boundary, with the connection, the models, and the migrations in their one place, the test swapping the database in through the proxy (Rule 5). The checklist is the five questions that catch the divergence - the model/schema drift, the uninspected query, the intent-less write, the tuned-instead-of-designed performance, the scattered data layer - and answered together, they are what keep the data layer a component the app is built on: the schema is the model's, the SQL is visible, the write is atomic and explicit, the performance is designed in, and the layer is one place a developer can read.