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
ForeignKey and relationship(), insert related data, and query associated objects using SQLAlchemy ORM.
Steps
- Create the Project Structure
- Configure the Database
- Create the User ORM Model
- Create the BlogPost ORM Model
- Implement the ORM Relationship
- Create the Database Tables
- Insert Sample Data
- Perform CRUD Operations
- 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.Initialize the Project
Initialize the Project
Install Dependencies
Install Dependencies
- The project is initialized using uv.
- The
apppackage is created. - All Python files are created.
- The required dependencies are added to
pyproject.toml. - The
uv.lockfile is generated.
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.
.env
.env
app/database.py and configure SQLAlchemy.
app/database.py
app/database.py
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_dbis created. .envfile contains theDATABASE_URL.database.pyloads the connection string successfully.- The SQLAlchemy engine and session factory are configured.
- No import or configuration errors are reported by your IDE.
Step 3: Create the User ORM Model
Objective Create theUser 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.
app/models.py
app/models.py
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: TheVerifyUsermodel currently represents a standalone table. The relationship with theBlogPostmodel will be implemented in a later step.
- The
UserRoleenum is created. - The
Usermodel inherits fromBase. - The
userstable schema matches the required design. - No import or syntax errors are reported by your IDE.
Step 4: Create the BlogPost ORM Model
Objective Create theBlogPost 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.
app/models.py
app/models.py
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
BlogPostmodel inherits fromBase. - The
blog_poststable schema matches the required design. - The
author_idcolumn referencesusers.id. - No import or syntax errors are reported by your IDE.
Step 5: Implement the ORM Relationship
Objective Implement a One-to-Many relationship between theUser 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.
User model by adding a posts relationship.
Update the User Model
Update the User Model
BlogPost model by adding an author relationship.
Update the BlogPost Model
Update the BlogPost Model
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:VerifyForeignKey()creates the relationship in the database, whereasrelationship()creates the relationship between Python objects. Both are required for a complete ORM relationship.
- The
Usermodel contains thepostsrelationship. - The
BlogPostmodel contains theauthorrelationship. - Both relationships use
back_populates. - No import or type hint errors are reported by your IDE.
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, whilerelationship()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 theUser and BlogPost models are defined, use SQLAlchemy to generate the corresponding tables in the database.
Open app/main.py and create the tables.
app/main.py
app/main.py
Create the Database Tables
Create the Database Tables
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: TheVerifyUserandBlogPostmodels must be imported before callingcreate_all(). Otherwise, SQLAlchemy will not detect them and the corresponding tables will not be created.
- The
userstable is created. - The
blog_poststable is created. - The
author_idforeign key is created successfully. - The application runs without errors.
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 theauthor_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.
app/seed.py
app/seed.py
Insert Sample Data
Insert Sample Data
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
userstable. - Three blog posts are inserted into the
blog_poststable. - The
author_idcolumn is populated automatically. - The seed script executes without errors.
Step 8: Query Related Data
Objective Query related data using the ORM relationships and navigate between users and blog posts without writing SQL joins. Instructions The relationships defined usingrelationship() 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.
app/main.py
app/main.py
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.
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 thejoin() method for this purpose.
Open app/main.py and execute the following join queries.
app/main.py
app/main.py
Execute the Application
Execute the Application
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 (Verifypost.author,user.posts) when navigating between related objects. Usejoin()when querying data from multiple tables with filtering, sorting, or aggregation.
- Blog posts are displayed along with their authors.
- The join query executes successfully.
- Results are returned without writing raw SQL.
Step 10: Update and Delete Related Data
Objective Update and delete related objects using ORM relationships. Instructions Retrieve an existing user and create a new blog post by assigning theauthor relationship. Then update an existing blog post and delete another one.
Open app/main.py and perform the following operations.
app/main.py
app/main.py
Run the Application
Run the Application
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.
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. Whenuser.posts is accessed, SQLAlchemy automatically executes another query to fetch the related blog posts.
Lazy Loading
Lazy Loading
joinedload().
Eager Loading with joinedload()
Eager Loading with joinedload()
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.
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.- Display all blog posts along with their author’s username.
- Display all blog posts written by a specific user.
- Display the total number of blog posts created by each user.
- Display only published blog posts.
- Update the title of a specific blog post.
- Delete a blog post by its ID.
- Create a new author and assign two blog posts to that author.
- Delete an author and observe how the
cascade="all, delete-orphan"option affects the related blog posts. - Retrieve all authors who have published at least one blog post.
- Display each user along with the number of blog posts they have written.
Challenge: Rewrite the queries using both ORM relationships (Verifyuser.posts,post.author) and explicitjoin()statements wherever applicable. Compare the readability of each approach and observe the SQL statements generated by SQLAlchemy.
- 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.