Skip to main content

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:

  1. Core—for executing raw SQL and building SQL expressions.

  2. 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)
Note:

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()
FeedbackOpens in a new tab