PythonSQLAlchemySQLitePostgreSQL

Making SQLAlchemy Queries

Alchemists working with potions and ingredients in a laboratory with query-like symbols floating above their work.

In this article, I'll go over how to make queries using both imperative and declarative approaches in SQLAlchemy. We'll begin with the imperative approach and move on to the declarative approach, exploring the various ways to interact with our database.

Preparation

In the last article we’ve seen how to define and migrate tables in both the imperative and declarative approaches, if you skipped that article I think it’s important to go back and read up about the differences in approaches and why both approaches even exist, as the fact that there are multiple ways of doing the same might be confusing at first.

We will begin be re-creating and migrating the tables we’ve created in the last article, and make sure our code works as part of this article.

from sqlalchemy import create_engine, Table, Column, Integer, String, ForeignKey, MetaData

# Create a metadata object to allow SQLAlchemy
# to keep track of all of the imperative models
metadata_obj = MetaData()
# Create our engine and connect to an in-memory SQLite database.
# Note that echo=True
engine = create_engine('sqlite:///:memory:', echo=True)

alchemists_table = Table(
  'alchemists', # The name of the table
  metadata_obj, # The metatada_obj defined earlier
  Column('id', Integer, primary_key=True), # For the sake of simplicity, we will use integer as an ID
  Column('name', String(45)),
  Column('nickname', String(100)),
  Column('age', Integer),
)

potions_table = Table(
  'potions',
  metadata_obj,
  Column('id', Integer, primary_key=True),
  Column('name', String(100)),
  Column('effect', String(100)),
  Column('duration', Integer),
)

brewed_potions_table = Table(
  'brewed_potions',
  metadata_obj,
  Column('id', Integer, primary_key=True),
  Column('potion_id', ForeignKey("potions.id"), nullable=False),
  Column('alchemist_id', ForeignKey("alchemists.id"), nullable=False),
)

flasks_table = Table(
"flasks",
  metadata_obj,
  Column('id', Integer, primary_key=True),
  Column('type', String(100)),
  Column('volume', Integer),
)

# Create all of the imperative models tracked by SQLAlchemy using the engine object
metadata_obj.create_all(engine)

Using echo=True

In some parts of this article series we will be using the echo=True flag when creating our engine, this will allow us to see all of the queries that are being sent to the database, and the results that are being returned.

This is a great way to debug our application, in this article series we will mostly use it for new topics, as I believe it is important to see what happens behind the scenes when writing SQLAlchemy code.

If you would like to turn it off, you can edit the code in the last code block to remove the echo=True flag.

Imperative Queries

Once our tables are declared and migrated, we can perform CRUD operations. Since our tables are declared imperatively, we do not create objects from them but leverage them to build database queries.

Inserting data into our tables

Let's begin by populating our tables with data. For this, we'll start by inserting information using a new approach that hasn't been covered in previous articles:

with engine.begin() as connection:
  edward = alchemists_table.insert().values(
      name='Edward Elric',
      nickname='The Fullmetal Alchemist',
      age=16
  )
  al = alchemists_table.insert().values(
      name='Alphonse Elric',
      nickname='Al',
      age=15
  )
  roy = alchemists_table.insert().values(
      name='Roy Mustang',
      nickname='Hero of Ishval',
      age=29
  )
  connection.execute(edward)
  connection.execute(al)
  connection.execute(roy)

We can also condense our inserts by writing something similar:

with engine.begin() as connection:
  stmt = alchemists_table.insert().values([
      {
          'name': 'Izumi Curtis',
          'nickname': 'Teacher',
          'age': 35
      },
      {
          'name': 'Van Hohenheim',
          'nickname': 'The Sage of the West',
          'age': 451
      }
  ])
  connection.execute(stmt)

Note that we are using the begin() function, which will automatically commit our insert statements (stmt) when the context manager exits.

In the previous chapter, we covered the difference between the begin() and connect() functions.

Insert with return

We can insert values and then make a select query to get what we just inserted, but obviously, there is a better way in which we construct a query that inserts data and returns it at the same time. We can do so by:

with engine.begin() as connection:
  stmt = flasks_table.insert().values([
      {
          'type': 'Volumetric Flask',
          'volume': 1000
      },
      {
          'type': 'Round Bottom Flask',
          'volume': 2000
      }
  ]).returning(
      flasks_table.c.id,
      flasks_table.c.type,
      flasks_table.c.volume
  )
  result = connection.execute(stmt)
  print(result.all())

As we can see, since the id column is automatically incremented, we can get it right after the insert using the returning function.

Selecting data

To select our data, we also have a very straightforward method to do so using SQLAlchemy:

from sqlalchemy import select

with engine.connect() as connection:
  stmt = select(
      alchemists_table.c.name,
      alchemists_table.c.nickname
  ).where(alchemists_table.c.age > 30)
  
  results = connection.execute(stmt)
  # Pull all of the results from the CursorResult object
  result_set = results.all()
  
  for result in result_set:
      # print(result.all())
      print(result)

In this query, we are using some new functions that were not covered in previous chapters:

  • select is a function that returns a select query expression; it expects either columns, or full table objects.
  • where is a function that exists on the select function that we can use in the construction of our query

To our select statement, we pass the column names we would like to have in our result.

When we query imperatively defined tables, we access the column using the .c.column_name accessor, just as we did in the previous chapter when we inspected our table's columns.

We do the same thing for our where statement, but when we do, we are actually passing a comparison expression to the where function, in this case:

alchemists_table.c.age > 30

If you are wondering how it is possible for us to make Pythonic comparisons on column definitions, behind the scenes, SQLAlchemy maps Python types to SQL types, which it represents as different classes, which allows it to define custom comparison operators for usage in the query builder (if you're interested in how that works, I recommend reading about dunder methods, also known as magic methods).

In fact, SQLAlchemy allows us to create our own custom types and comparison operations.

This means that if our program had some complex datatypes for which we would want to create custom comparisons, we could do that in a pretty straightforward way.

Updating data

Updating data is just as simple as selecting and inserting. Let's say we want to update the age of an alchemist:

from sqlalchemy import update

with engine.begin() as connection:
  stmt = update(alchemists_table).where(
      alchemists_table.c.name == 'Edward Elric'
  ).values(
      age=17
  )
  result = connection.execute(stmt)
  print(f"Updated {result.rowcount} rows")

In this query, we're using the update function from SQLAlchemy. We first specify which table we want to update, then we use the where function to specify which rows we want to update, and finally, we use the values function to specify what values we want to set.

We can also update multiple rows at once:

with engine.begin() as connection:
  stmt = update(alchemists_table).where(
      alchemists_table.c.age < 18
  ).values(
      nickname=alchemists_table.c.nickname + ' (Minor)'
  )
  result = connection.execute(stmt)
  print(f"Updated {result.rowcount} rows")

In this example, we're updating the nickname of all alchemists who are minors, appending " (Minor)" to their nickname. Notice how we can use column references in the values function as well.

Deleting data

Deleting data is similar to selecting and updating. Let's say we want to delete an alchemist:

from sqlalchemy import delete

with engine.begin() as connection:
  stmt = delete(alchemists_table).where(
      alchemists_table.c.name == 'Van Hohenheim'
  )
  result = connection.execute(stmt)
  print(f"Deleted {result.rowcount} rows")

In this query, we're using the delete function from SQLAlchemy. We first specify which table we want to delete from, then we use the where function to specify which rows we want to delete.

We can also delete multiple rows at once:

with engine.begin() as connection:
  stmt = delete(alchemists_table).where(
      alchemists_table.c.age > 400
  )
  result = connection.execute(stmt)
  print(f"Deleted {result.rowcount} rows")

In this example, we're deleting all alchemists who are older than 400 years.

It's worth noting that when we delete data from a table, any data that depends on it through foreign key constraints will also be affected.

Depending on how the foreign key constraint is set up, the dependent data might be deleted (CASCADE), set to NULL (SET NULL), or the delete operation might be prevented (RESTRICT).

Joining tables

Now that we have inserted data into our tables, we can start joining them to get more complex results. Let's say we have already created and populated our potions_table and brewed_potions_table from the previous chapter:

from sqlalchemy import join, select

with engine.connect() as connection:
  # First, let's create a join object
  j = alchemists_table.join(
      brewed_potions_table,
      alchemists_table.c.id == brewed_potions_table.c.alchemist_id
  ).join(
      potions_table,
      brewed_potions_table.c.potion_id == potions_table.c.id
  )
  
  # Now, let's use this join in a select statement
  stmt = select(
      alchemists_table.c.name,
      potions_table.c.name.label('potion_name')
  ).select_from(j)
  
  # Execute the query and get the results
  result = connection.execute(stmt)
  
  # Print the results
  for row in result:
      print(f"{row.name} brewed {row.potion_name}")

In this query, we're first creating a join object j that joins the alchemists_table with the brewed_potions_table on the id and alchemist_id columns, and then joins the result with the potions_table on the potion_id and id columns.

Then, we're using this join object in a select statement to get the name of the alchemist and the name of the potion. We're using the label function to give the potion name a different label in the result, so we can access it as row.potion_name.

We can also perform more complex joins, such as outer joins:

from sqlalchemy import select, outerjoin

with engine.connect() as connection:
  # Create an outer join
  j = alchemists_table.outerjoin(
      brewed_potions_table,
      alchemists_table.c.id == brewed_potions_table.c.alchemist_id
  ).outerjoin(
      potions_table,
      brewed_potions_table.c.potion_id == potions_table.c.id
  )
  
  # Use the join in a select statement
  stmt = select(
      alchemists_table.c.name,
      potions_table.c.name.label('potion_name')
  ).select_from(j)
  
  # Execute and get results
  result = connection.execute(stmt)
  
  # Print results
  for row in result:
      potion_name = row.potion_name if row.potion_name else "no potion"
      print(f"{row.name} brewed {potion_name}")

In this example, we're using outer joins, which means that we'll get all alchemists, even those who haven't brewed any potions. For those alchemists, the potion_name will be None, which is why we're using a conditional to replace None with "no potion".

Declarative Queries

Now that we've covered the imperative approach, let's move on to the declarative approach.

Declarative queries tend to look a little bit different than their imperative counterparts. They are much more ORM-like, as they use objects and classes much more, when working with the Declarative approach you get the benefits of working with very specific types, most IDE’s and text-editors will know how to assist you when working with this approach, leaving the guessing game out of the development flow.

Defining our tables

In the last chapter we’ve taken a look at how to create declarative type models, lets recreate them and move on to using them.

from sqlalchemy.orm import DeclarativeBase, mapped_column, Mapped
from sqlalchemy import String, Integer, ForeignKey

class BaseModel(DeclarativeBase):
  pass


class Alchemist(BaseModel):
  __tablename__ = 'alchemists'
  id: Mapped[int] = mapped_column(primary_key=True)
  name: Mapped[str] = mapped_column(String(45), nullable=False)
  nickname: Mapped[str] = mapped_column(String(100))
  age: Mapped[int]


class Potion(BaseModel):
  __tablename__ = 'potions'
  id: Mapped[int] = mapped_column(Integer, primary_key=True)
  name: Mapped[str] = mapped_column(String(100))
  effect: Mapped[str] = mapped_column(String(100))
  duration: Mapped[int] = mapped_column(Integer)

class BrewedPotion(BaseModel):
  __tablename__ = 'brewed_potions'
  id: Mapped[int] = mapped_column(Integer, primary_key=True)
  potion_id: Mapped[int] = mapped_column(ForeignKey("potions.id"), nullable=False)
  alchemist_id: Mapped[int] = mapped_column(ForeignKey("alchemists.id"), nullable=False)


BaseModel.metadata.create_all(engine)

Creating a Session

To query declarative models, we need to create a Session:

from sqlalchemy.orm import Session

# Create a session
with Session(engine) as session:
  # Now we can query our declarative models
  pass

The Session object is a factory for creating SQL statements and handles object state management.

Inserting Data

To insert data using the declarative approach, we create instances of our models and add them to the session:

from sqlalchemy.orm import Session

with Session(engine) as session:
  # Create a new alchemist
  edward = Alchemist(name='Edward Elric', nickname='Fullmetal Alchemist', age=16)
  
  # Add it to the session
  session.add(edward)
  
  # Create and add multiple alchemists at once
  al = Alchemist(name='Alphonse Elric', nickname='Al', age=15)
  roy = Alchemist(name='Roy Mustang', nickname='Hero of Ishval', age=29)
  van = Alchemist(name='Van Hohenheim', nickname='The Sage of the West', age=451)
  
  session.add_all([al, roy, van])
  
  healing_potion = Potion(name='Healing Potion', effect='Heals 50 HP', duration=0)
  strength_potion = Potion(name='Strength Potion', effect='Increases strength by 10', duration=300)
  invisibility_potion = Potion(name='Invisibility Potion', effect='Grants invisibility', duration=60)
  
  session.add_all([healing_potion, strength_potion, invisibility_potion])
  session.commit() 

  bp1 = BrewedPotion(alchemist_id=al.id, potion_id=healing_potion.id)
  bp2 = BrewedPotion(alchemist_id=roy.id, potion_id=strength_potion.id)
  bp3 = BrewedPotion(alchemist_id=van.id, potion_id=invisibility_potion.id)
  
  session.add_all([bp1, bp2, bp3])
  session.commit()
  # Commit the changes
  session.commit()
  
  # Now the alchemists have IDs
  print(f"Edward's ID: {edward.id}")

In this example, we're creating instances of our Alchemist model and adding them to the session. The add method adds a single instance, while the add_all method adds multiple instances at once. After we commit the session, the instances will have their IDs set.

Selecting Data

To select data using the declarative approach, we can use the query method of the session:

with Session(engine) as session:
  # Get all alchemists
  alchemists = session.query(Alchemist).all()
  for alchemist in alchemists:
      print(f"{alchemist.name}, {alchemist.nickname}, {alchemist.age}")
  
  # Get alchemists older than 30
  older_alchemists = session.query(Alchemist).filter(Alchemist.age > 30).all()
  for alchemist in older_alchemists:
      print(f"{alchemist.name} is {alchemist.age} years old")

In this example, we're using the query method to get all alchemists, and then to get alchemists older than 30 years old. The all method returns a list of all results.

We can also use the newer style with the select function:

from sqlalchemy import select

with Session(engine) as session:
  # Get all alchemists
  stmt = select(Alchemist)
  alchemists = session.scalars(stmt).all()
  for alchemist in alchemists:
      print(f"{alchemist.name}, {alchemist.nickname}, {alchemist.age}")
  
  # Get alchemists older than 30
  stmt = select(Alchemist).where(Alchemist.age > 30)
  older_alchemists = session.scalars(stmt).all()
  for alchemist in older_alchemists:
      print(f"{alchemist.name} is {alchemist.age} years old")

In this newer style, we're using the select function to create a select statement, and then we're using the scalars method of the session to execute the statement and get a list of scalar results.

Updating Data

To update data using the declarative approach, we first select the objects we want to update, change their attributes, and then commit the session:

with Session(engine) as session:
  # Get Edward Elric
  edward = session.query(Alchemist).filter_by(name='Edward Elric').first()
  
  # Update his age
  edward.age = 17
  
  # Commit the changes
  session.commit()

In this example, we're first getting the alchemist named Edward Elric, then we're updating his age, and finally we're committing the changes to the database.

We can also update multiple objects at once:

with Session(engine) as session:
  # Get all minor alchemists
  minors = session.query(Alchemist).filter(Alchemist.age < 18).all()
  
  # Update their nicknames
  for minor in minors:
      minor.nickname = minor.nickname + ' (Minor)'
  
  # Commit the changes
  session.commit()

In this example, we're getting all alchemists who are minors, updating their nicknames, and committing the changes.

Deleting Data

To delete data using the declarative approach, we first select the objects we want to delete, call the delete method on them, and then commit the session:

with Session(engine) as session:
  # Get the alchemist named Van Hohenheim
  hohenheim = session.query(Alchemist).filter_by(name='Van Hohenheim').first()
  
  # Delete him
  session.delete(hohenheim)
  
  # Commit the changes
  session.commit()

In this example, we're first getting the alchemist named Van Hohenheim, then we're deleting him, and finally we're committing the changes to the database.

We can also delete multiple objects at once:

with Session(engine) as session:
  # Get all alchemists older than 400 years
  old_alchemists = session.query(Alchemist).filter(Alchemist.age > 400).all()
  
  # Delete them
  for alchemist in old_alchemists:
      session.delete(alchemist)
  
  # Commit the changes
  session.commit()

In this example, we're getting all alchemists who are older than 400 years, deleting them, and committing the changes.

Joining Tables

To join tables using the declarative approach, we can use the join method of the query object:

with Session(engine) as session:
  # Query alchemists and their potions
  query = session.query(
      Alchemist.name,
      Potion.name.label('potion_name')
  ).join(
      BrewedPotion,
      Alchemist.id == BrewedPotion.alchemist_id
  ).join(
      Potion,
      BrewedPotion.potion_id == Potion.id
  )
  
  # Execute the query and get the results
  results = query.all()
  
  # Print the results
  for result in results:
      print(f"{result.name} brewed {result.potion_name}")

In this example, we're joining the Alchemist, BrewedPotion, and Potion tables to get a list of alchemists and the potions they've brewed.

In the next article, we will delve into relationships, which are another way of joining between tables using our defined models.

Relationships also allow much more complex connections, and access to related models that might not be as straightforward to implement using native SQL queries.

Conclusion

In this article, we've learned how to make queries using both the imperative and declarative approaches in SQLAlchemy. We've covered inserting, selecting, updating, deleting, and joining data. These are the fundamental operations that you'll use in almost any application that interacts with a database.

The imperative approach is more SQL-like and gives you precise control over the queries, while the declarative approach is more Pythonic and lets you work with objects. Both have their strengths and are useful in different scenarios.

Next, we'll explore SQLAlchemy relationships, how to implement and use them, and how they allow us to write much more straightforward ORM queries.

Author image
I'm Yonatan Vega
Owner & Creator, Fullstack.rocks
I've enjoyed writing code ever since I discovered it was an option. At a young age I started by writing code for MMORPG's private servers, and hosting my own private servers at home.

Today I'm writing and architecting software using many different technologies, and I'm always looking for the next thing to learn.