Access Relational Data as Objects with SQLAlchemy
While the previous exercise used DB-API to use InterSystems IRIS as a relational database from Python, this exercise shows you how to use relational data as objects using SQLAlchemy.
Prerequisites
Before starting this exercise, make sure that you have installed SQLAlchemy and DB-API using the following command:
pip install sqlalchemy-intersystems-iris
See the DB-API exercise for a brief description of these tools.
SQLAlchemy: SQL Toolkit and ORM
SQLAlchemy is a powerful SQL toolkit and ORM tool built on top of DB-API. It provides a higher-level abstraction over SQL, allowing you to define tables as Python classes and interact with them using ORM. It is ideal for larger applications that require cleaner, more maintainable code and helps you avoid raw SQL when possible.
SQLAlchemy has two major components:
-
Core—for executing raw SQL and building SQL expressions.
-
ORM—for mapping Python classes to database tables and managing transactions.
Establishing an Engine Connection
To connect to InterSystems IRIS using SQLAlchemy, use the create_engine() function with the appropriate dialect and connection string. The dialect is specified before the :// in the DATABASE_URL. The engine is the central source of connection and is used for both the Core and ORM.
from sqlalchemy import create_engine
# Replace with your credentials and connection information
username = "_SYSTEM"
password = "SYS"
namespace = "USER"
DATABASE_URL = f"iris://{username}:{password}@localhost:1972/{namespace}"
engine = create_engine(DATABASE_URL, echo=True) # Set echo=True to see the SQL queries being executed in the terminal
Just like in DB-API, you can execute simple static queries via the .execute() method:
# SQL statements get wrapped in text sequences
from sqlachemy import text
# Connect to the database and execute a static query
with engine.connect() as conn:
query = text("SELECT * FROM Sample.Person WHERE ID = 1")
result = conn.execute(query)
row = result.fetchone()
print(row)
Transactions and Sessions
InterSystems IRIS supports robust transaction management through SQLAlchemy, whether you are using the Core or ORM interface. Through this, you have control over transaction states, commits, roll backs, and much more.
engine.begin()
The engine.begin() method is a context manager that starts a transaction. It starts a transaction block that automatically commits when the block exists without an error.
# engine.begin starts a transaction block with an auto-commit
with engine.begin() as conn:
result = conn.execute(text("SELECT Name, Age, FROM Sample.Person"))
for row in result:
print(f"Name: {row.Name}\nAge: {row.Age}\n")
engine.connect()
The engine.connect() starts a transaction block that automatically rolls back when the block exists. To save any changes made in the transaction, you must call to conn.commit() to commit your changes.
# engine.connect starts a transaction block with an automatic roll back
with engine.connect() as conn:
conn.execute(demo_table.insert(), [{"id": 1, "value": "Test"}],)
conn.commit() # Need to explictly commit to save changes
Session(engine)
Then Session object provides a high-level interface for managing transactions and interacting with ORM objects. It establishes a conversation between the database and your code, giving you fine-grained control over commits, rollbacks, and object states.
# Session starts an ORM session with fine-grained commit/rollback controls
from sqlalchemy.orm import Session
print("Listing rows where Age > 50:")
stmt = text("SELECT Name, Age FROM Sample.Person WHERE Age > :min_age ORDER BY Age, Name")
with Session(engine) as session:
result = session.execute(stmt, {"min_age", 50})
for row in result:
print(f"Name: {row.Name}\nAge: {row.Age}\n")
SQLAlchemy Core: Fine-Grained SQL Control
SQLAlchemy Core provides a schema-first, lower-level approach for building and executing SQL statements using Python constructs. It gives you precise control over SQL generation and execution making it ideal for:
-
Complex or dynamic SQL queries.
-
Performance-sensitive applications.
-
Developers who prefer SQL-like control without raw strings.
Defining Tables with SQL Alchemy Core
You can define tables using the Table() and Column() constructors and then execute SQL statements using the select(), insert(), update(), and delete() functions. Remember to first create the table metadata. This allows SQLAlchemy to keep track of the table structure and determine whether it needs to create the table in the database when calling create_all() on the metadata object.
from sqlalchemy import Column, MetaData, Table
from sqlalchemy.sql.sqltypes import Integer, VARCHAR
# Create a table metadata
metadata = MetaData()
# Define the table structure
demo_table = Table(
"demo_table",
metadata,
Column("id", Integer, primary_key = True, autoincrement = True),
Column("value", VARCHAR(50)),
)
# Insert sample data
with engine.connect() as conn:
conn.execute(
demo_table.insert(),
[
{"id": 1, "value": "Test"},
{"id": 2, "value": "More"},
],
)
conn.commit()
result = conn.execute(demo_table.select()).fetchall()
print("result:", result)
Tables created using this method will be seen as SQL.User.<table_name> under the InterSystems IRIS Management Portal.
SQLAlchemy ORM: Object-Oriented Data Access
The ORM layer abstracts away the relational structure of your data allows you to define tables as Python classes. In this way, you work with data in a more “Pythonic” way as you and interact with data using traditional Python objects.
It is ideal for:
-
Create, read, update, delete (CRUD) operations.
-
Business logic encapsulation.
-
Applications that benefit from abstraction.
Creating ORM Models
ORM allows you to define database tables as Python classes. Each class represents a table, and each attribute represents a column. This abstraction simplifies database operations and integrates well with Python applications. All tables inherit from the Base class (which itself inherits from DeclarativeBase).
from typing import List, Optional
from sqlalchemy import String, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class User(Base):
__table__name = "user_account"
# 'id' column is the primary key of the table
id: Mapped[int] = mapped_column(primary_keys=True)
# 'name' column has a maximum string length of 30 characters
name: Mapped[str] = mapped_column(String(30))
# 'fullname' column is optional, so it can be NULL in the database
fullname: Mapped[Optional[str]]
# String representation of the object; useful for debugging
def __repr__(self) ->:
return f"User(id = {self.id!r}, name = {self.name!r}, fullname = {self.fullname!r})"
The general format for defining an object is:
columnName: Mapped[type] = mapped_column(db.Type, arguments)
Saving Data
To save data using ORM, create an instance of the model class and add it to a session. Then commit the session to persist the changes to the database.
Base.metadata.create_all(engine) # Create the table if it does not exist
with Session(engine) as session:
# Create new instances of the User class
frodo = User(
name = "frodo",
fullname = "Frodo Baggins"
)
samwise = User(
name = "samwise",
fullname = "Samwise Gamgee"
)
pippin = User(
name = "pippin",
fullname = "Pippin Took")
# Add all three new User objects to the session (prepares them to be inserted)
session.add_all([frodo, samwise, pippin])
# Commit the session; writes the changes (inserts) to the database
session.commit()
Retrieving Data
To retrieve data using ORM, use the Session object to query the model class. You can use filters and other query methods to refine your search results.
from sqlalchemy import select # 'select' is used to build SQL SELECT queries
with Session(engine) as session:
# Print users with specific names
stmt = select(User).where(User.name.in_(["frodo", "samwise"]))
# 'scalars()' extracts the actual User objects from the result
for user in session.scalars(stmt):
print(user)
Deleting Data
To delete records using SQLAlchemy ORM, you first query the object you want to remove, then pass it to the session’s delete() method. Finally, commit the session to apply the change to the database.
with Session(engine) as session:
# List of IDs to delete
ids_to_delete = [1, 2, 3]
for user_id in ids_to_delete:
user = session.get(User, user_id)
if user:
session.delete(user)
session.commit()