12.7. Updating Records
Table of Contents
Understanding Record Updates with an ORM
Updating records is one of the four core database operations, often grouped under CRUD, which stands for Create, Read, Update, Delete. In this chapter, you will learn how to update data when you use an ORM, with a focus on the mental model and typical patterns rather than on one specific framework.
You already know what an ORM is and how to create and read records from earlier chapters. Here we focus on what is unique about updating.
The Basic Update Flow
Although ORMs differ in syntax, most follow the same logical steps when you want to update something:
- Load the record from the database into an object.
- Modify attributes on that object.
- Mark changes for saving by telling the ORM you want to persist them.
- Commit the transaction so the changes are actually written to the database.
In pseudo code, a typical update looks like this:
# 1. Read the record
user = session.get(User, user_id)
# 2. Change some fields
user.email = "new_email@example.com"
user.is_active = True
# 3. Tell the ORM to save changes (often implicit)
session.add(user)
# 4. Commit
session.commit()In many ORMs, step 3 is optional once the object is attached to the session. The important idea is that the ORM tracks changes on loaded objects and writes them to the database when you commit.
Updating a Single Record
Updating a single record usually looks like updating a normal object in memory.
Step 1: Load the record
First get the record you want to change:
user = session.get(User, 42) # 42 is the primary keyIf the record might not exist, you must handle that case:
user = session.get(User, 42)
if user is None:
# handle "not found", for example:
raise ValueError("User not found")Step 2: Modify fields
You change attributes just like normal Python attributes:
user.name = "Alice Updated"
user.email = "alice.updated@example.com"
You can also use setattr if you want to update fields programmatically:
for field, value in {"name": "Alice Updated", "email": "alice.updated@example.com"}.items():
setattr(user, field, value)Step 3: Commit the changes
Once you are done changing attributes:
session.commit()
The ORM generates an UPDATE statement similar to:
UPDATE users
SET name = 'Alice Updated',
email = 'alice.updated@example.com'
WHERE id = 42;
The ORM only does this when you call commit (or sometimes flush).
Partial Updates
You do not need to set every field on the record. You can update just one or two attributes.
Examples:
# Update only the email
user.email = "new@example.com"
session.commit()
# Update only a boolean flag
user.is_active = False
session.commit()The ORM tracks which fields actually changed. Good ORMs generate SQL that updates only the modified fields. This improves performance and avoids accidental overwrites.
Sometimes you receive only some fields from an API request. A common pattern is:
def update_user_from_payload(user, data: dict):
# data might look like {"name": "Alice", "is_active": True}
for key, value in data.items():
if hasattr(user, key):
setattr(user, key, value)Then:
user = session.get(User, user_id)
if user is None:
raise ValueError("User not found")
update_user_from_payload(user, payload_dict)
session.commit()This maps naturally to HTTP PATCH semantics in REST APIs.
Bulk Updates
Sometimes you need to update many rows at once, for example when you deactivate all users who have not logged in for a year.
With an ORM you have two main strategies:
1. Load every record and update in Python
old_users = (
session.query(User)
.filter(User.last_login < one_year_ago)
.all()
)
for user in old_users:
user.is_active = False
session.commit()Pros:
- The ORM enforces validation and triggers any hooks / events on the Python objects.
- You can perform complex logic inside Python.
Cons:
- Loads all matching records into memory. This can be slow and heavy for large datasets.
2. Use a bulk update query
Many ORMs support a method that directly translates to a SQL UPDATE query without loading each record:
session.query(User).filter(User.last_login < one_year_ago).update(
{User.is_active: False}
)
session.commit()This generates one SQL statement:
UPDATE users
SET is_active = FALSE
WHERE last_login < :one_year_ago;Pros:
- Very fast and memory efficient for large numbers of rows.
Cons:
- Bypasses Python side logic on individual objects.
- The in memory objects for those rows, if they exist in the session, might be out of date until refreshed.
Important rule: Use bulk updates for performance, but remember they usually skip per object validation and events. Only use them when you are sure about the effect of the raw SQL.
Handling Not Found and Optional Updates
When you try to update something that may not exist, you must handle the missing case gracefully.
Example: Update or create error
user = session.get(User, user_id)
if user is None:
# Decide what to do: error or create new
raise ValueError(f"User with id {user_id} not found")
user.email = new_email
session.commit()In a web API, you usually convert this to an HTTP 404 response.
Optional field updates
Sometimes the input contains None for fields. You must decide what None means:
- It might mean "do not change this field".
- It might mean "set this field to NULL in the database".
One common pattern is to distinguish between "field is missing from the input" and "field is present with value null". For example:
def update_user(user, data):
if "name" in data:
user.name = data["name"] # even if None
if "email" in data:
user.email = data["email"]
If "name" is absent from data, you leave user.name as it is.
Concurrency and Lost Updates
Imagine two requests update the same user at nearly the same time.
- Request A loads the user.
- Request B loads the same user.
- Request A sets
email = "a@example.com"and commits. - Request B sets
name = "New Name"and commits.
If request B updates all fields from its in memory object, it might overwrite the email change from request A. This is called a lost update.
There are two common strategies to protect against this.
1. Let the database handle it with transactions
You can use transactions and isolation levels so that updates are serialized. Some ORMs also support "select for update" queries:
user = (
session.query(User)
.filter(User.id == user_id)
.with_for_update()
.one()
)This locks the row until the transaction commits.
2. Optimistic locking with a version field
You can add a version column that increments on every update.
Example table:
| Column | Type |
|---|---|
| id | integer |
| name | text |
| text | |
| version | integer |
Workflow:
- Load the user, including
version. - When updating, send
versionback. - The ORM generates an update that checks the version:
UPDATE users
SET name = :name,
email = :email,
version = version + 1
WHERE id = :id AND version = :version;- If no row is updated (row count is 0), it means someone else already changed it. You can then raise a concurrency error.
Many ORMs have built in support for this pattern.
Important rule: In concurrent systems, always think about lost updates. Use transactions, locks, or version columns to protect critical data.
Updates and Transactions
Every update should be part of a transaction. In most ORMs, one session.commit() corresponds to committing a transaction.
You can combine several operations:
# Start transaction
user = session.get(User, user_id)
order = session.get(Order, order_id)
user.last_order_id = order.id
order.status = "processed"
# Both updates are committed together
session.commit()
If anything fails before the commit, you call session.rollback() and the database discards all partial changes.
This is important for maintaining consistent data.
Updating Related Records
When you have relationships, you can update related records through the object graph. This uses the ORM features you learned in the relationships chapter.
Example: Update a one to many relationship
Suppose a User has many Address entries:
user = session.get(User, user_id)
# Update the first address line
addr = user.addresses[0]
addr.line1 = "New Street 123"
session.commit()
The ORM notices that an Address object changed and generates an UPDATE for the addresses table.
Replacing an entire collection
To replace all existing addresses with a new list:
user.addresses = [
Address(line1="Street 1", city="Paris"),
Address(line1="Street 2", city="Berlin"),
]
session.commit()Depending on configuration, the ORM might:
- Insert new rows and delete old ones, or
- Try to update existing ones.
You must understand the relationship settings in your ORM so you know what exactly happens when you reassign collections.
Avoiding Common Mistakes
1. Forgetting to commit
If you change attributes but do not call commit, nothing is persisted.
user = session.get(User, user_id)
user.email = "new@example.com"
# No commit, so no SQL UPDATE is sentIn a request handling function, make sure you either:
- Commit explicitly, or
- Use a helper that commits automatically at the end if everything succeeds.
2. Updating detached objects
Sometimes you create or load an object outside the current session. Then you try to commit:
user = User(id=42, email="new@example.com") # not loaded from session
session.add(user)
session.commit()
If the ORM thinks this is a new object, it might try to INSERT instead of UPDATE. To update an existing row without loading it, many ORMs have a special method, for example "merge", or you use a query based update.
Better, load the object from the session first, then modify:
user = session.get(User, 42)
user.email = "new@example.com"
session.commit()3. Accidentally overwriting data
If you convert an entire request body to a model and assign it directly, you may overwrite fields that were not part of the request or that the user should not control.
Instead, explicitly choose which fields can be updated:
allowed_fields = ["name", "email"]
for field in allowed_fields:
if field in data:
setattr(user, field, data[field])This is called a whitelist approach and is safer.
When to Use ORM Updates vs Raw SQL
Most of the time, ORM based updates are enough and make your code clearer. However, sometimes raw SQL is better.
Use ORM style updates when:
- You update a few records.
- You want to reuse model validations or business logic.
- You want to work with rich objects, not rows.
Use raw SQL or bulk operations when:
- You update very large sets of rows.
- You need very specific or complex SQL behavior.
- Performance is critical and you know exactly what you are doing.
You can always mix approaches in one application. The key is to be consistent within a given use case and to document when you bypass the ORM.
Summary
- Updating with an ORM follows the same pattern: load, modify, commit.
- You can do partial updates by changing only selected attributes.
- Bulk updates are efficient for many rows, but often skip per object logic.
- Always handle the "not found" case when loading records to update.
- Think about concurrency to avoid lost updates, especially in multi user systems.
- Wrap updates in transactions so that related changes succeed or fail together.
- When updating relationships, treat them like normal attributes, but know how your ORM handles cascades and collection changes.
- Avoid common errors such as forgetting to commit, updating detached objects incorrectly, or overwriting fields you did not intend to change.
With these patterns, you can safely and predictably change data through your ORM, which is essential for building reliable backend applications.
Views: 6
KAHIBARO