Using the DAO

The previous page concluded with access to a DAO instance. This page explains the basic operations available through your new DAO interface.

For the examples on this page, we'll assume the following sample DAOs.

from daomodel import DAO, DAOModel
from daomodel.fields import Identifier

class Customer(DAOModel, table=True):
    id: Identifier[int]
    name: str
    email: str

# Model with a composite primary key
class OrderDetail(DAOModel, table=True):
    order_id: Identifier[int]
    line_number: Identifier[str]
    product_code: Identifier[bytes]
    quantity: int

dao = DAO(Customer, db)
order_dao = DAO(OrderDetail, db)

Note: db in the above code is from Getting Started.
Identifier defines a field as a Primary Key which will be described on the Model page.

CRUD Operations

The DAO class provides a complete set of Create, Read, Update, and Delete operations for your DAOModel instances. These operations make it easy to interact with your database without writing SQL queries.

Create

Creating new records in your database is straightforward with the DAO.

create

create(*pk_values: Any) -> Model

Creates a new entry for the given primary key.

Parameters:
  • pk_values (Any, default: () ) –

    Primary key values to represent the record (in the order defined in the model)

Returns:
  • Model

    The DAOModel entry that was newly added to the database

Raises:
  • PrimaryKeyConflict

    if an entry already exists for the primary key

  • InvalidArgumentCount

    if the provided values do not align with the model's primary key

# Create a new customer with just the primary key
customer = dao.create(1) # Creates a Customer with id=1

# Create an OrderDetail having multiple primary key values
order = order_dao.create(42, 'A123', b'PRD001') 

If you wish to set more properties than just the primary key, you will want to use the create_with method.

create_with

create_with(insert: bool = True, **values: Any) -> Model

Creates a new entry for the given primary key and property values.

Providing a DAOModel as a value extracts the object's primary key value.

Parameters:
  • insert (bool, default: True ) –

    False to avoid adding the record to the database

  • values (Any, default: {} ) –

    The values to assign to the new record

Returns:
  • Model

    The new DAOModel

Raises:
  • PrimaryKeyConflict

    if an entry already exists for the primary key (does not apply if insert=False)

  • UnsupportedFeatureError

    if a DAOModel value has a composite primary key

# Create a customer with specific values
customer = dao.create_with(id=3, name='John Smith', email='john@example.com')

Note: The create_with method requires the primary key values to be provided as keyword arguments.

If you wish to create an instance without adding it to the database, you can set the insert parameter to False.

customer = dao.create_with(id=4, name='Guest User', insert=False)

If using insert=False you will then need to explicitly call insert if you wish to add the row to the DB.

insert

insert(model: Model) -> None

Adds the given model record to the database.

Parameters:
  • model (Model) –

    The DAOModel entry to add

Raises:
  • PrimaryKeyConflict

    if an entry already exists for the primary key

# Create a customer without inserting it
customer = dao.create_with(id=5, name='Alice Brown', email='alice@example.com', insert=False)

# Insert it into the database
dao.insert(customer)

After adding to the database, you will now be able to read the data.

Read

The DAO provides several methods to retrieve data from your database.

get

get(*pk_values: Any) -> Model

Retrieves an entry from the database by its primary key.

Parameters:
  • pk_values (Any, default: () ) –

    The primary key values of the record to fetch (in the order defined in the model)

Returns:
  • Model

    The DAOModel entry that was retrieved

Raises:
  • NotFound

    if the entry does not exist in the database

  • InvalidArgumentCount

    if the provided values do not align with the model's primary key

# Get a customer by primary key
customer = dao.get(1)  # Gets Customer with id=1

# Get an order detail with a composite primary key
order = order_dao.get(42, 'A123', b'PRD001')  # Again, order must match the model definition

The get_with method expands upon get by optionally applying additional values.

get_with

get_with(**values: Any) -> Model

Retrieves an entry from the database and applies the given values to it.

These changes are not committed to the database. Call commit() to do so.

Parameters:
  • values (Any, default: {} ) –

    A dictionary containing the pk values of the requested entry along with additional values to set

Returns:
  • Model

    The DAOModel entry with the additional properties updated

Raises:
  • NotFound

    if the entry does not exist in the database

# Get a customer and modify its properties
customer = dao.get_with(id=1, name='Updated Name')

Note: Like create_with, the get_with method requires keyword arguments for the primary key.

If you do not know the primary key values, you can search for a record based on other properties.

find

find(_page: Optional[int] = None, _per_page: Optional[int] = None, _order: Optional[str | Column | UnaryExpression | Iterable[str | Column | UnaryExpression]] = None, _duplicate: Optional[str] = None, _unique: Optional[str] = None, _having: Optional[dict[str, tuple[str, Any]]] = None, **filters: Any) -> SearchResults[Model]

Searches all the DAOModel entries to return results.

Parameters:
  • _page (Optional[int], default: None ) –

    The number of the page to fetch

  • _per_page (Optional[int], default: None ) –

    How many results are on each page

  • _order (Optional[str | Column | UnaryExpression | Iterable[str | Column | UnaryExpression]], default: None ) –

    How to sort the results

  • _duplicate (Optional[str], default: None ) –

    Filter the results to only duplicate values of a column

  • _unique (Optional[str], default: None ) –

    Filter the results to only unique values of a column

  • _having (Optional[dict[str, tuple[str, Any]]], default: None ) –

    Filter the results to only values having a specified number of relationships

  • filters (Any, default: {} ) –

    Criteria to filter down the number of results

Returns:
  • SearchResults[Model]

    The SearchResults for the provided filters

Searching is covered in more detail on the Search page.

If you only need to know if an object is in the DB, the exists method can help with that.

exists

exists(model: Model) -> bool

Determines if a record exists in the database.

Parameters:
  • model (Model) –

    The DAOModel entry in question

Returns:
  • bool

    True if the entry exists in the database, False otherwise

# Check if a customer exists by creating a temporary instance
temp_customer = Customer(id=1)
if dao.exists(temp_customer):
    print('Customer exists!')

Note: If you don't have the model instance, a get call within a try block may be simpler.

Update

The DAO provides methods to easily update existing records.

commit

commit(*models_to_refresh: DAOModel) -> None

Commits all pending changes to the database.

'Pending changes' includes data changes made to models that were fetched from the database. Use dao.start_transaction() to avoid automatically calling this method following each insert, upsert, and remove. This will commit all changes within the session and is not limited to this DAO. Following the DB commit, DAOModels will be detached, needing to be refreshed.

If this DAO was in transaction mode, it will be reset to auto-commit mode after committing.

Parameters:
  • models_to_refresh (DAOModel, default: () ) –

    The DAOModels to refresh after committing

Raises:
  • Conflict

    if a unique constraint is violated

  • IntegrityError

    if a database constraint is violated (e.g., NOT NULL, foreign key)

# Get a customer and update it
customer = dao.get(1)
customer.name = 'New Name'
dao.commit()  # Save changes

# Or use get_with to update in less steps
customer = dao.get_with(id=1, name='Another Name')
# Save changes and refresh the customer reference to continue using it
dao.commit(customer)
print(customer.name)  # prints 'Another Name'

Alternatively, you can use the upsert method to store the updated record regardless of if it exists.

upsert

upsert(model: Model) -> None

Updates the given model record in the database or creates it if it does not exist.

Parameters:
  • model (Model) –

    The DAOModel entry which may or may not exist

# Create a customer that may or may not exist
customer = Customer(id=10, name='Maybe New', email='maybe@example.com')

# Create row if id=10 doesn't exist, or update row if already present
dao.upsert(customer)

If it is the primary key values you need to change, both commit and upsert may not work as you intend. The rename method handles this for you.

rename

rename(existing: Model, *new_pk_values: Any) -> None

Updates the given model record with new primary key values.

Parameters:
  • existing (Model) –

    The record to rename

  • new_pk_values (Any, default: () ) –

    The new primary key values for the record

Raises:
  • PrimaryKeyConflict

    if an entry already exists for the new primary key

# Get an existing customer
customer = dao.get(1)

# Rename (change primary key)
dao.rename(customer, 100)  # Changes id from 1 to 100

# When renaming composite keys, pass all primary key values in order
order = order_dao.get(42, 'A123', b'PRD001')
# Change order_id, and product_code (but not line_number)
order_dao.rename(order, 43, 'A123', b'PRD002')

Delete

Finally, if you need to delete a record, the DAO provides that method too.

remove

remove(model: Model) -> None

Deletes the given model entry from the database.

Parameters:
  • model (Model) –

    The DAOModel object to be deleted

Raises:
  • NotFound

    if the model does not exist in the database

# Get a customer
customer = dao.get(1)

# Remove it
dao.remove(customer)

# Or remove using a temporary instance
dao.remove(Customer(id=2))  # raises NotFound if it isn't in the database

Transactions

The DAO supports transactions to ensure data integrity when performing multiple operations. A transaction is a sequence of database operations that are treated as a single unit of work. Either the transaction completes all the operations (committed), or none of them (rolled back). This ensures data consistency and helps maintain the integrity of your database.

To better understand this concept, let's consider an example. If you're transferring money between two accounts, you want both the withdrawal and deposit to either succeed together or fail together - you don't want one to happen without the other. Transactions provide this "all-or-nothing" guarantee.

Starting a Transaction

To enter transaction mode, use the start_transaction method.

start_transaction

start_transaction() -> None

Starts a transaction by setting transaction_mode to True.

This disables auto_commit until the transaction is committed or rolled back.

# Start a transaction
dao.start_transaction()

try:
    # Perform multiple operations
    customer1 = dao.create_with(id=1, name='Transaction Test 1')
    customer2 = dao.create_with(id=2, name='Transaction Test 2')

    # Commit the transaction
    dao.commit()
except Exception as e:
    # Rollback on error
    dao.rollback()
    print(f'Transaction failed: {e}')

Transactions Across Multiple DAOs

When multiple DAOs share the same database session (such as when they're created from the same DAOFactory), transactions are automatically coordinated across all of them. This means:

  1. Starting a transaction on one DAO affects all DAOs sharing the same session
  2. Changes made through any DAO in the session are part of the same transaction
  3. Committing or rolling back the transaction affects all changes made through any DAO in the session

This is particularly useful when your operations span multiple model types:

# Get DAOs for different model types
student_dao = daos[Student]
book_dao = daos[Book]

# Start a transaction on the factory
daos.start_transaction()

try:
    # Create a student
    student = student_dao.create_with(id=100, name='Alice')

    # Create a book for the student using a different DAO
    book = book_dao.create_with(name='Physics', subject='Science', owner=100)

    # Commit the transaction - both student and book are saved
    daos.commit()
except Exception as e:
    # Rollback on error - neither student nor book is saved
    daos.rollback()
    print(f'Transaction failed: {e}')

Note: You can call the transaction models on the DAOFactory or an individual DAO from the Factory.

commit

As you can see above, commit() is how you finalize a transaction. It saves all pending changes to the database and exits transaction mode. If you wish to follow up with an additional transaction, you can call start_transaction again. If you wish to back out of all changes instead of committing them, you can see that is done through a rollback.

rollback

rollback() -> None

Reverts all pending database changes of a transaction.

This will discard all changes that have not yet been committed.

Raises:
  • RuntimeError

    if not in transaction mode

dao.start_transaction()

# Make some changes
customer = dao.create_with(id=100, name='Will be rolled back')

# Decide to cancel these changes
dao.rollback()

# The customer won't be in the database
try:
    dao.get(100)  # This will raise NotFound
except NotFound:
    print('Customer was not created due to rollback')

It may not be obvious at this time if you will want to use transactions, but just remember it is here to simplify the process if you do find you need to group some actions together.

Query

The DAO provides access to SQLAlchemy's powerful query interface for other, unsupported operations.

query = dao.query

# Example: Update all customers with '@example.com' email to have 'Example Customer' as their name
query.filter(Customer.email.like('%@example.com')).update(
    {'name': 'Example Customer'},
    synchronize_session=False
)

# Don't forget to commit the changes
dao.commit()

All the information you need for SQLAlchemy querying can be found in their ORM Querying Guide. So if something is not supported directly with DAOModel, you are able to use SQLAlchemy instead. Or, if you feel it is a good feature, submit a ticket to request it be added to DAOModel.

Next Steps

Now that you understand how to use the DAO, let's move on to the details of Models on the next page. You will see how the DAOModel library can vastly reduce the amount of code it takes to design your database tables.