Skip to main content

Implementing ORM Relationships in the Blog Posts Application

We will build a simple Blog Posts application to learn how relationships are implemented using SQLAlchemy ORM. The focus of this workshop is on creating related ORM models and working with them through Python objects. The application consists of two entities:
  • Users
  • Blog Posts
Each user can create multiple blog posts, and each blog post belongs to exactly one user. This represents a One-to-Many relationship, one of the most common relationship types used in real-world applications. By the end of this workshop, you will understand how to define relationships using ForeignKey and relationship(), insert related data, and query associated objects using SQLAlchemy ORM.

Steps

  1. Create the Project Structure
  2. Configure the Database
  3. Create the User ORM Model
  4. Create the BlogPost ORM Model
  5. Implement the ORM Relationship
  6. Create the Database Tables
  7. Insert Sample Data
  8. Perform CRUD Operations
  9. Query Related Data

Step 1: Create the Project Structure

Objective Create the initial project structure for the Blog Posts ORM application and initialize the project using uv. Instructions Create a new project directory and initialize it as a Python project. Create the following project structure.
Project Overview
Install the required dependencies.
** Verify ** Verify the following before proceeding:
  • The project is initialized using uv.
  • The app package is created.
  • All Python files are created.
  • The required dependencies are added to pyproject.toml.
  • The uv.lock file is generated.
** Commit **

Step 2: Configure the Database

Objective Configure SQLAlchemy by creating the database engine, session factory, and declarative base. The database connection string will be stored in a .env file and loaded into the application at runtime. Instructions Create a PostgreSQL database named blog_posts_db. Store the database connection string in a .env file instead of hardcoding it in your application. This keeps sensitive information such as database credentials separate from the source code. Create a .env file in the project root.
Open app/database.py and configure SQLAlchemy.
The load_dotenv() function loads all variables from the .env file into the application. The os.getenv() function reads the value of DATABASE_URL. The SQLAlchemy engine manages the database connection, SessionLocal creates database sessions, and Base acts as the parent class for all ORM models.
Note: It is a best practice to store configuration values such as database URLs, API keys, and secrets in environment variables instead of hardcoding them in your source code.
Verify
  • PostgreSQL database blog_posts_db is created.
  • .env file contains the DATABASE_URL.
  • database.py loads the connection string successfully.
  • The SQLAlchemy engine and session factory are configured.
  • No import or configuration errors are reported by your IDE.
Commit

Step 3: Create the User ORM Model

Objective Create the User ORM model that represents the users table in the database. This model stores user information and will later be associated with blog posts through an ORM relationship. Instructions The users table stores information about the application’s users. Each user has a unique username and email address. The role column identifies whether the user is an AUTHOR or an ADMIN, and the created_at column records when the user was created. Create the following table schema. Open app/models.py and define the User ORM model.
The User class inherits from Base, making it an ORM model. The __tablename__ attribute specifies the database table name, while mapped_column() maps each class attribute to its corresponding database column.
Note: The User model currently represents a standalone table. The relationship with the BlogPost model will be implemented in a later step.
Verify
  • The UserRole enum is created.
  • The User model inherits from Base.
  • The users table schema matches the required design.
  • No import or syntax errors are reported by your IDE.
Commit

Step 4: Create the BlogPost ORM Model

Objective Create the BlogPost ORM model that represents the blog_posts table in the database. Each blog post belongs to a user, so this table includes a foreign key that references the users table. Instructions The blog_posts table stores information about blog posts created by users. Each blog post has a title, content, publication status, and an author. The author_id column is a Foreign Key that references the id column of the users table. Create the following table schema. Open app/models.py and add the BlogPost model below the User model.
The author_id column creates a database relationship by referencing the primary key of the users table. At this stage, only the Foreign Key is defined. The ORM relationship using relationship() will be implemented in the next step.
Note: A foreign key enforces referential integrity at the database level, ensuring that every blog post is associated with an existing user.
Verify
  • The BlogPost model inherits from Base.
  • The blog_posts table schema matches the required design.
  • The author_id column references users.id.
  • No import or syntax errors are reported by your IDE.
Commit

Step 5: Implement the ORM Relationship

Objective Implement a One-to-Many relationship between the User and BlogPost models using SQLAlchemy’s relationship() function. Instructions The author_id column created in the previous step establishes the relationship at the database level using a foreign key. In this step, you will create the ORM relationship, allowing you to navigate between related Python objects. The relationship is shown below.
Update the User model by adding a posts relationship.
Update the BlogPost model by adding an author relationship.
The posts relationship allows a user to access all of their blog posts, while the author relationship allows a blog post to access its author. The back_populates parameter links both sides of the relationship, keeping them synchronized. The cascade="all, delete-orphan" option automatically deletes a user’s blog posts when the user is removed.
Note: ForeignKey() creates the relationship in the database, whereas relationship() creates the relationship between Python objects. Both are required for a complete ORM relationship.
Verify
  • The User model contains the posts relationship.
  • The BlogPost model contains the author relationship.
  • Both relationships use back_populates.
  • No import or type hint errors are reported by your IDE.
Commit
The posts relationship allows a user to access all of their blog posts, while the author relationship allows a blog post to access its author. The back_populates parameter links both sides of the relationship, keeping them synchronized. The cascade="all, delete-orphan" option automatically applies ORM operations such as save, update, and delete to related blog posts. It also ensures that all blog posts belonging to a user are deleted automatically when the user is deleted, preventing orphan records from remaining in the database.
Note: ForeignKey() establishes the relationship at the database level, while relationship() creates the relationship between Python objects, allowing you to navigate related data using the ORM.

Step 6: Create the Database Tables

Objective Create the database tables from the ORM models using SQLAlchemy. Instructions Now that the User and BlogPost models are defined, use SQLAlchemy to generate the corresponding tables in the database. Open app/main.py and create the tables.
Run the application.
The Base.metadata.create_all() method scans all ORM models that inherit from Base and creates the corresponding tables in the connected database. If a table already exists, SQLAlchemy skips its creation.
Note: The User and BlogPost models must be imported before calling create_all(). Otherwise, SQLAlchemy will not detect them and the corresponding tables will not be created.
Verify
  • The users table is created.
  • The blog_posts table is created.
  • The author_id foreign key is created successfully.
  • The application runs without errors.
Commit

Step 7: Insert Sample Data

Objective Insert sample users and blog posts into the database and establish the relationship between them using SQLAlchemy ORM. Instructions Create two users and assign a few blog posts to each user. Instead of setting the author_id manually, assign the User object to the author relationship. SQLAlchemy will automatically populate the foreign key when the changes are committed. Open app/seed.py and insert the sample data.
Run the seed script.
Notice that the author relationship is assigned directly with a User object instead of manually setting the author_id. During session.commit(), SQLAlchemy automatically stores the correct foreign key value in the blog_posts table.
Note: Using ORM relationships makes the code more readable and object-oriented by allowing you to work with Python objects instead of managing foreign key values manually.
Verify
  • Two users are inserted into the users table.
  • Three blog posts are inserted into the blog_posts table.
  • The author_id column is populated automatically.
  • The seed script executes without errors.
Commit
Objective Query related data using the ORM relationships and navigate between users and blog posts without writing SQL joins. Instructions The relationships defined using relationship() allow you to navigate between related objects. A User object can access all of its blog posts using the posts relationship, and a BlogPost object can access its author using the author relationship. Open app/main.py and query the related data.
Run the application.
The posts relationship returns all blog posts written by a user, while the author relationship returns the user who wrote a particular blog post. SQLAlchemy automatically retrieves the related objects when they are accessed, eliminating the need to manually write SQL join queries.
Note: Although SQLAlchemy executes SQL queries behind the scenes, you interact with Python objects instead of writing SQL statements directly.
Verify
  • All users are displayed.
  • Each user displays their associated blog posts.
  • Each blog post displays its author.
  • The application executes without errors.
Commit

Step 9: Execute Join Queries

Objective Retrieve data from multiple related tables using SQLAlchemy joins. Instructions While ORM relationships allow you to navigate related objects, there are situations where you need to retrieve data from multiple tables in a single query. SQLAlchemy provides the join() method for this purpose. Open app/main.py and execute the following join queries.
Run the application.
The join() method combines the blog_posts and users tables based on the foreign key relationship. Unlike relationship navigation, joins allow you to filter, sort, and retrieve data from multiple tables efficiently within a single query.
Note: Use ORM relationships (post.author, user.posts) when navigating between related objects. Use join() when querying data from multiple tables with filtering, sorting, or aggregation.
Verify
  • Blog posts are displayed along with their authors.
  • The join query executes successfully.
  • Results are returned without writing raw SQL.
Commit
Objective Update and delete related objects using ORM relationships. Instructions Retrieve an existing user and create a new blog post by assigning the author relationship. Then update an existing blog post and delete another one. Open app/main.py and perform the following operations.
Run the application.
The new blog post is associated with the user by assigning the author relationship instead of manually setting the author_id. SQLAlchemy automatically manages the foreign key and persists the relationship when the session is committed. Verify
  • A new blog post is created.
  • The new post is associated with the correct user.
  • An existing blog post is updated successfully.
  • The newly created blog post is deleted successfully.
Commit

Step 11: Explore Relationship Loading

Objective Understand how SQLAlchemy loads related objects and learn the difference between lazy loading and eager loading. Instructions By default, SQLAlchemy uses lazy loading, which means related objects are loaded only when they are accessed. For example, the following code first retrieves all users. When user.posts is accessed, SQLAlchemy automatically executes another query to fetch the related blog posts.
To load users and their blog posts together in a single query, use joinedload().
With lazy loading, SQLAlchemy executes an additional query whenever a related collection is accessed. With eager loading, the related objects are fetched along with the parent object, reducing the number of database queries and improving performance when related data is needed.
Note: Use lazy loading when related data is not always required. Use eager loading when you know the related objects will be accessed immediately.
Verify
  • Users and their blog posts are displayed correctly.
  • Both lazy loading and eager loading produce the same results.
  • Observe the SQL statements in the terminal (echo=True) and compare the number of queries executed.
Commit

Step 12: Practice Exercises

Objective Practice working with SQLAlchemy ORM relationships by implementing a few additional queries and operations on the Blog Posts application. Instructions Complete the following exercises using the concepts learned in this workshop.
  1. Display all blog posts along with their author’s username.
  2. Display all blog posts written by a specific user.
  3. Display the total number of blog posts created by each user.
  4. Display only published blog posts.
  5. Update the title of a specific blog post.
  6. Delete a blog post by its ID.
  7. Create a new author and assign two blog posts to that author.
  8. Delete an author and observe how the cascade="all, delete-orphan" option affects the related blog posts.
  9. Retrieve all authors who have published at least one blog post.
  10. Display each user along with the number of blog posts they have written.
Challenge: Rewrite the queries using both ORM relationships (user.posts, post.author) and explicit join() statements wherever applicable. Compare the readability of each approach and observe the SQL statements generated by SQLAlchemy.
Verify
  • All exercises execute successfully.
  • The expected records are created, updated, queried, and deleted.
  • The generated SQL statements match the intended operations.
  • You can confidently navigate between related objects using ORM relationships.
Commit