All Python developers have something to gain from SQLAlchemy. Whether you're looking for a better way to manage database connections or build out an ORM data layer for your application, there's no reason for any of us to omit pip install sqlalchemy from our Python vocabulary.
Engineers working on large-scale applications overwhelmingly prefer handling data via ORMs over raw SQL. For those with heavy data backgrounds (like myself), the abstraction of hiding SQL behind Python objects can be off-putting. Why do we need foreign keys to execute JOINs between two tables? Why do engineers working on large-scale software seem to overuse terminology such as "one-to-many" versus "many-to-many" relationships, when SQL itself has no such terminology? If you've felt this sentiment, you're in good company.
After a couple of years, I've come to find that ORMs do result in less work in the context of building applications, and aren't just a crutch for people who are "afraid of SQL." We save significant time by handling sensitive data transactions as reproducible code patterns, but this benefit pales compared to what we gain in security and integrity. ORMs don't write destructive SQL queries; people do.
So, yes. It is annoying that engineers who write ORMs use different lingo than those who understand the underlying SQL, and it is annoying how much upfront work it takes to set up an ORM, but it is worth it. Today we're getting the hardest part of ORM development out of the way by learning how to define table relationships in SQLAlchemy.
Set up Some Data Models
We already covered SQLAlchemy data models in a previous post, so I'll skip the finer details. If you arrived here by frantically Googling questions about SQLAlchemy, you should probably catch up on what models are and how to define them.
We will create a few models to demonstrate how to create a SQL relationship between them. In the spirit of blogging, we'll be creating models for User, Post, and Comment:
One-to-Many Relationships
One-to-many (or many-to-one) relationships are the most common type of database relationships. A timeless example of how such a relationship is applied is a business' relationship between customers & orders. Single customers have multiple orders, but orders don't have multiple customers, hence the term
Using our blog example, let's look at what a one-to-many relationship might look like for authors who have multiple posts or posts that have multiple comments:
We added two critical additionals to our models which might be hard to spot at first glance. First, we set some attributes (columns) as Foreign keys (if you're familiar with SQL, you should be good to go here). A foreign key is a property of a column; when a foreign key is present, we're saying that this particular column denotes a relationship between tables: most common items of one table "belong" to items of another table, like when customers "own" orders, or when users "own" posts. In our example, we're saying that each post has an author (user) as specified by the author_id attribute, seen here:
We can marry data between our User table and our Posttable so that fetching one will allow us to get information about the other.
The other new concept here is relationships. Relationships complement foreign keys and are a way of telling our application(not our database) that we're building relationships between two models. Notice how the value of our foreign key is 'user.id'. useris the table name for our User table. Compare this to the value we pass to our relationship, which is "User": the class name of the target data model (not the table name!).
Foreign keys tell SQL which relationships we're building, and relationships tell our app which relationships we're building. We need to do both.
The point is the ability to easily perform JOINs in our app. When using an ORM, we wouldn't be able to say "JOIN this model with that model", because our app would have no idea which columns to JOINon. When our relationships are specified in our models, we can do things like join two tables together without specifying any further detail: SQLAlchemy will know how to join tables/models by looking at what we set in our data models (as enforced by the foreign keys & relationships we set). We're just saving ourselves the burden of dealing with data-related logic while creating our app's business logic by defining relationships upfront.
💡
SQLAlchemy only creates tables from data models if the tables don't already exist. In other words, if we have faulty relationships the first time we run our app, the error messages will persist the second time we run our app, even if we think we've fixed the problem. To deal with strange error messages, try deleting your SQL tables before running your app again whenever changing a model.
Back References
Specifying relationships on a data model allows us to access properties of the joined model via a property on the original model. If we were to join our Comment model with our User model, we'd be able to access the properties of a comment's author via Comment.user.username, where user is the name of our relationship, and username is a property of the associated model.
Relationships created in this way are one-directional in that we can access Team details through Player, but can't access Player details from Team directly. We can solve this easily by setting a backreference.
When creating a relationship, we can pass an attribute called backref to make a relationship bi-directional. Here's how we'd modify the relationship we set previously:
With a backref present, we can now access user details of a post by calling Post.author.
Creating Related Data
Time to create some data. We will need a user to serve as our blog's author. While we're at it, let's give them some blog posts:
We created our objects, but we haven't saved them to our database yet. I'm going to throw together a couple of functions for this; create_user() will handle user creation, and create_post() will create... well, you know:
If you're caught off-guard by the complexity of these functions, know that everything besides session.add() and session.commit() exists for error handling and avoiding duplicates. We don't want duplicate posts or users, hence why we check for existing_user and existing_post before continuing with their respective functions.
Let's create these records:
We now have one user, and many (ish) posts! If you were to check your database, you would see these records created there. The stage is set for a one-to-many join query.
Performing a One-to-Many JOIN
When we perform a JOIN on SQLAlchemy models, we can utilize the relationship attribute of each record we fetch as though the attribute were an entire record. It's probably easier just to show you what I mean.
In the query below, we fetch all posts in our database belonging to user 1. We then JOIN posts belonging to that user by extending our query with .join(User, Post.author_id == User.id):
After executing the query, we loop over each record and print a JSON object representing the posts we've fetched along with their author's data:
Many-to-Many Relationships
Setting foreign key relationships serves us well when we expect a table in our relationship to only have a single record per multiple records in another table (ie: one player per team). What if players could belong to multiple teams? This is where things get complicated.
As you might've guessed, many-to-many relationships happen between tables where n number of records from Table 1 could be associated with n number of records from a table. SQLAlchemy achieves relationships like these via association tables. An association table is a SQL table created for the sole purpose of explaining these relationships, and we're going to build one.
Check out how we define the association_table variable below:
We're using a new data type Table to define a table that builds a many-to-many association. The first parameter we pass is the name of the resulting table, which we name association. Next, we pass Base.metadata to associate our table with the same declarative base that our data models extend. Lastly, we create two columns that serve as foreign keys to each table we're associating: we're linking Player's team_ida column with Team's id column.
The essence of what we're doing here is creating a third table that associates our two tables. We could also achieve this by creating a third data model, but creating an association table is more straightforward. From here on out, we can now query association_table directly to get records from our Player and Team tables.
The final step of implementing an association table is to set a relationship on our data model. Notice how we set a relationship on Player as we did previously, but this time we set the secondary attribute equal to the name of our association table.
Wasted youth as a Product Manager, enjoying life as a Lead Software Engineer. Happily retired from rapidly propping up doomed startups. Focused on creating meaningful work and relationships.
Monthly Newsletter
Support us
We share tutorials to help and inspire new engineers and enthusiasts. If you've found Hackers and Slackers to be helpful, we welcome donations in the form of coffee :).