Aug 14th, 2020 - written by Kimserey with .
In a previous post, we talked about Alembic which is a migration tool built on top of SQLAlchemy. SQLAlchemy is a Python SQL toolkit and Object Relational Mapper (ORM). In today’s post, we will look at how the ORM part can be leverage to easily create and manage a simple database schema.
This post is heavily inspired from the official SQLAlchemy ORM Tutorial.
SQLAlchemy supports all major SQL engines, Postgres
, MySQL
, Oracle
, MSSQL
, SQLite
, etc… In this post we will be using SQLite
as it provides an in memory storage, quick and easy for prototyping. We start by creating the engine and specifying our connection string:
1
2
3
from sqlalchemy import create_engine
engine = create_engine("sqlite:///:memory:", echo=True)
echo=True
will echo all SQL commands.
We then define our database models:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
profile = relationship("UserProfile", uselist=False, back_populates="user")
def __repr__(self):
return "<User(id={}, name={}>".format(self.id, self.name)
class UserProfile(Base):
__tablename__ = "user_profile"
user_id = Column(Integer, ForeignKey("user.id"), primary_key=True)
user = relationship("User", back_populates="profile")
job = Column(String)
address = Column(String)
def __repr__(self):
return "<UserProfile(user_id={}, job={}, address={})>".format(
self.user_id, self.job, self.address
)
declarative_base
creates a base class, which here we call Base
, which contains the catalog of clases and tables in a metadata
property. Each table mapping defined can be retrived from Base.metadata.tables
.
1
2
3
> Base.metadata.tables
immutabledict({'user': Table('user', MetaData(bind=None), Column('id', Integer(), table=<user>, primary_key=True, nullable=False), Column('name', String(), table=<user>), schema=None), 'user_profile': Table('user_profile', MetaData(bind=None), Column('user_id', Integer(), ForeignKey('user.id'), table=<user_profile>, primary_key=True, nullable=False), Column('job', String(), table=<user_profile>), Column('address', String(), table=<user_profile>), schema=None)})
Here we defined User
and UserProfile
tables by giving them a name using the __tablename__
attribute and defining columns with Column
.
In order to define an object property relationship, we use relationship
from sqlalchemy.orm
. In User
, profile
will not be a column in the table but rather a relationship with UserProfile
. Similarly, UserProfile.user
will be a relationship with User
. SQLAlchemy knows how to use the existing foreign key on user to make either a lazy select, or an eager join.
Once we’re done creating our model, we can create the database schema by executing:
1
Base.metadata.create_all(engine)
By looking at the logs, we should be able to spot the CREATE TABLE
queries indicating that our tables were created.
Now that we have our database models and database schema created, we can start inserting data. To start interacting with the database, we need to get hold of a session.
1
2
3
4
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)
session = Session()
A Session
class is created using the sessionmaker
which generates a new class with the settings provided to the sessionmaker
like autoflush
or autocommit
. Here we directly bind the Session
with our SQLite engine. Doing that we can be sure that when we instantiate sessions, we get a session connecting to our SQLite engine.
We then create a few users:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from faker import Faker
# Faker is used to auto generate comprehensible example of name/job/address.
fake = Faker()
def create_user():
user = User(name=fake.name())
user.profile = UserProfile(job=fake.job(), address=fake.address())
session.add(user)
for _ in range(20):
create_user()
session.commit()
We can see that we can use our models and instiate via constructor even though we have not specified any __init__
, this is because the Base
provides a default constructor that allows initialization from kwargs
.
And we are able to access the get or set the properties of the model to the actual types. This is made possible by the SQLAlchemy instrumentation which replaces all the columns properties by Python descriptors making it possible to assign directly the user name with user.name = "abc"
and similarly allowing us to assign the profile to a UserProfile
instance.
We then just add the user
to the session with session.add
. Notice that SQLAlchmey will know to add the UserProfile
instance and we don’t have to explicitly add it to the session.
And finally we commit the changes to the session which will execute the SQL Query.
1
INFO sqlalchemy.engine.base.Engine ((21, 'Graphic designer', '275 Gregory Port\nNew Debbie, DC 71038'), (22, 'Web designer', '25805 Williams Glen Suite 382\nWest William, WV 68028'), (23, 'Administrator, local government', '80675 Brian Bridge\nPort Marcuschester, SD 59259'), (24, 'Lobbyist', '099 Shannon Creek Apt. 617\nAndersonland, IA 20753'), (25, 'Retail buyer', '06512 Garcia Roads Suite 504\nEast Maryfort, NM 74890'), (26, 'Teacher, primary school', '828 Torres Isle\nLarsonport, LA 83446'), (27, 'Call centre manager', '48376 Snyder Flats\nHensonburgh, MO 82689'), (28, 'Operational investment banker', '62107 Katherine Underpass\nTracyhaven, WA 07744') ... displaying 10 of 20 total bound parameter sets ... (39, 'Public relations account executive', '067 Scott Centers\nWest Mirandaport, VA 62169'), (40, 'Geophysicist/field seismologist', '90011 Schmidt Causeway Suite 313\nLake Ryanborough, FL 55528'))
Now that we have inserted some users and profiles, we can look at how we would be able to run simple queries against our model. In order to run queries, we need to use the session
which we defined earlier and build queries using the Query
object.
1
users = session.query(User).all()
We query our model using .query()
and specifying the selection. If we want to get all our users, we can specify query(User)
or all our profile query(UserProfile)
. The output of query(...)
is a query object, in order to execute the query, we need to force the iteration either with a loop
or with a selector like .all
, .one
, .first
or a slice operator which will be translated into a combination of skip/take
. Returning a scalar can be done with .scalar
which will return the first column of the row as a value.
1
2
3
4
5
6
7
8
9
session.query(User.name, UserProfile.address).filter(
User.id == UserProfile.user_id
).first()
session.query(User, UserProfile.address).filter(
User.id == UserProfile.user_id
).order_by(User.name)[2:5]
session.query(User.name).filter(User.id == 2).scalar()
We can also specify a mixture of selection, for example User.name, UserProfile.address
will return a list of tuples str, str
while User, UserProfile.address
will return an instrumented User
model with a str
.
To apply filters to a query, we use filter
. The most common operators are ==
, !=
, .like
, .in_
, and_
and or_
.
1
2
3
4
5
6
7
8
9
10
11
12
13
from sqlalchemy import or_
session.query(UserProfile).filter(UserProfile.address.like("%Stravenue%")).all()
session.query(UserProfile).filter(
or_(UserProfile.address.like("%Stravenue%"), UserProfile.job.like("%school%"))
).all()
session.query(UserProfile).filter(
or_(UserProfile.address.like("%Stravenue%"), UserProfile.job.like("%school%"))
).all()
session.query(User).filter(User.name.in_(["Terry Franklin", "John Johnson"])).all()
We saw earlier that the quickest way to join table is to use an implicit query by selection both tables and using a filter on the identifier.
1
2
3
session.query(User, UserProfile).filter(
User.id == UserProfile.user_id
)
This will create an implicit join and return the user and profile. If we don’t need to return the profile, we can instead use a SQL join:
1
session.query(User).join(UserProfile).filter(UserProfile.job.like("%school%")).all()
Lastly we mentioned during the creation of our model that the relationships were lazily loaded by default. This means that .profile
will not be loaded until we access it resulting in a SELECT
statement. In some cases we might want to earger load all profile. This can be achieved with .options(joinedload(...))
.
1
2
3
4
5
6
7
8
9
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import joinedload
users = session.query(User).options(joinedload(User.profile))
# Compare with: session.query(User)
for user in users:
print(user.profile.job)
After executing the loop, we will see that we don’t incur extra SELECT
statements. And that concludes today’s post!
Today we saw how we could get started with SQLAlchemy ORM. We started by looking at what an engine was, then moved on to define a simple database model which we then created on a in memory SQLite database. We then moved on to see how we could insert data, and lastly we looked at how we could query the data we inserted using the powerful Query
API provided by SQLAlchemy. I hope you liked this post and I see you on the next one!