Defining Schema in SQLAlchemy ORM
Last updated on July 27, 2020
SQLAlchemy ORM #
SQLAlchemy ORM (Object Relational Mapper) is a way to define tables and relationship between them using Python classes. It also provides a system to query and manipulate the database using object-oriented code instead of writing SQL. Unlike SQLAlchemy Core, which is focused around tables, rows and columns; the ORM is focused around objects and models.
ORM is built upon the SQLAlchemy Core, so everything you have learned so far still applies.
As you will see, an ORM allows you to be more productive but it also adds additional overhead to your queries. However, for most applications, its benefits far outweighs the performance penalty.
Note: Before moving on to the next section delete all the tables from the sqlalchemy-tuts
database using the following command:
metadata.drop_all(engine)
Defining Models #
A Model is a Python class which corresponds to the database table and its attributes represent the columns.
For the class to be a valid model, it must do the following:
- Inherit from a declarative base class created by calling
declarative_base()
function. - define table name via
__tablename__
attribute. - define at-least one column which must be a part of the primary key.
The last two points are self-explanatory but the first one deserves a bit of explanation.
The base class maintains a catalog of classes and tables. In other words, the declarative base class wraps the mapper and the MetaData
. The mapper maps the subclass to the table and MetaData
holds all the information about the database and the tables it contains. Just as in Core, in ORM we use create_all()
and drop_all()
methods of the MetaData
object to create and drop tables.
The following listing defines a Post
model which can be used to store blog posts.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from sqlalchemy import create_engine, MetaData, Table, Integer, String, \
Column, DateTime, ForeignKey, Numeric
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
Base = declarative_base()
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
title = Column(String(100), nullable=False)
slug = Column(String(100), nullable=False)
content = Column(String(50), nullable=False)
published = Column(String(200), nullable=False, unique=True)
created_on = Column(DateTime(), default=datetime.now)
updated_on = Column(DateTime(), default=datetime.now, onupdate=datetime.now)
|
Let's step through the code line by line.
- In line 1-4, we import necessary classes and functions.
- In line 6, we create a base declarative class by calling the
declarative_base()
function. - In line 10-16, we define columns as class attributes.
Notice that we are using the same Column
class to define the columns as we did while using SQLAlchemy Core. The only difference is that now the first argument is type instead of the column name. Also, notice that the keyword arguments passed to the Column()
works exactly the same in ORM and Core.
Since ORM is built upon Core, SQLAlchemy will use the model definition to create a Table
object and associate it with the model by calling the mapper()
function. This completes the process of mapping the Post
model with its corresponding Table
instance. We can now use Post
model to query and manipulate the database.
We can peek at the Table
instance associated with the model using the __table__
attribute.
1 2 3 4 5 6 7 8 9 10 11 | >>>
>>> Post.__table__
Table('posts', MetaData(bind=None),
Column('id', Integer(), table=<posts>, primary_key=True, nullable=False),
Column('title', String(length=100), table=<posts>, nullable=False),
Column('slug', String(length=100), table=<posts>, nullable=False),
Column('content', String(length=50), table=<posts>, nullable=False),
Column('published', String(length=200), table=<posts>, nullable=False), Column('created_on', DateTime(), table=<posts>, default=ColumnDefault(<function datetime.now at 0x7f226c152bf8>)),
Column('updated_on', DateTime(), table=<posts>, onupdate=ColumnDefault(<function datetime.now at 0x7f226c162268>), default=ColumnDefault(<function datetime.now at 0x7f226c1621e0>)),
schema=None)
>>>
|
Classical Mappings #
After reading the above section you might get an impression that to use SQLAlchemy's ORM, you will have to rewrite your existing Table
instances as Models. However, this is simply not true.
It turns out that you can map any plain Python class to a Table
instance using the mapper()
function. Here is an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | from sqlalchemy import MetaData, Table, String, Column, Text, DateTime, Boolean
from sqlalchemy.orm import mapper
from datetime import datetime
metadata = MetaData()
post = Table('post', metadata,
Column('id', Integer(), primary_key=True),
Column('title', String(200), nullable=False),
Column('slug', String(200), nullable=False),
Column('content', Text(), nullable=False),
Column('published', Boolean(), default=False),
Column('created_on', DateTime(), default=datetime.now)
Column('updated_on', DateTime(), default=datetime.now, onupdate=datetime.now)
)
class Post(object):
pass
mapper(Post, post)
|
The mapper()
class takes two arguments: class to be mapped and the Table
object.
Just by doing this the Post
class will have attributes that corresponds to the columns of the table. Thus, the Post
class now has following attributes:
post.id
post.title
post.slug
post.content
post.published
post.created_on
post.updated_on
The code in the above listing is functionally equivalent to the Post
model we defined earlier.
Now you should have a good idea of what declarative_base()
is doing for us behind the scenes.
Adding Keys and Constraints #
When using ORM, we add keys and constraints to model using the __table_args__
attribute.
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 | class User(Base):
__tablename__ = 'users'
id = Column(Integer)
username = Column(String(100), nullable=False)
email = Column(String(100), nullable=False)
password = Column(String(200), nullable=False)
__table_args__ = (
PrimaryKeyConstraint('id', name='user_pk')
UniqueConstraint('username'),
UniqueConstraint('email'),
)
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
title = Column(String(100), nullable=False)
slug = Column(String(100), nullable=False)
content = Column(String(50), nullable=False)
published = Column(String(200), nullable=False, default=False)
user_id = Column(Integer(), nullable=False)
created_on = Column(DateTime(), default=datetime.now)
updated_on = Column(DateTime(), default=datetime.now, onupdate=datetime.now)
__table_args__ = (
ForeignKeyConstraint(['user_id'], ['users.id']),
Index('title_content_index' 'title', 'content'), # composite index on title and content
)
|
Defining Relationships #
One to Many #
We create a one-to-many relationship by placing a foreign key on the child class. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Author(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
first_name = Column(string(100), nullable=False)
last_name = Column(String(100), nullable=False)
books = relationship("Book")
class Book(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
title = Column(string(100), nullable=False)
copyright = Column(SmallInteger, nullable=False)
author_id = Column(Integer, ForeignKey('authors.id'))
|
The line author_id = Column(Integer, ForeignKey('authors.id'))
establishes a one-to-many relationship between the Author
and Book
model.
The relationship()
function adds attributes on the models to access the related data. At its minimum, it accepts the name of the class representing the target of the relationship.
The line books = relationship("Book")
adds a books
attribute to the Author
class.
Given an Author
object a
, we can now access books written by him using a.books
.
What if we want to access book's author from a Book
object?
One way to achieve this is to define separate relationship()
on Author
model like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Author(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
first_name = Column(string(100), nullable=False)
last_name = Column(String(100), nullable=False)
books = relationship("Book")
class Book(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
title = Column(string(100), nullable=False)
copyright = Column(SmallInteger, nullable=False)
author_id = Column(Integer, ForeignKey('authors.id'))
author = relationship("Author")
|
Given a Book
object b
, we can now access its author as b.author
.
Alternatively, we can use the backref
parameters to specify the attribute name to be added on the other side of the relationship.
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Author(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
first_name = Column(string(100), nullable=False)
last_name = Column(String(100), nullable=False)
books = relationship("Book", backref="book")
class Book(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
title = Column(string(100), nullable=False)
copyright = Column(SmallInteger, nullable=False)
author_id = Column(Integer, ForeignKey('authors.id'))
|
Further, you can define relationship()
on any side of the relationship. Thus, the preceding code can also be written as:
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Author(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
first_name = Column(string(100), nullable=False)
last_name = Column(String(100), nullable=False)
class Book(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
title = Column(string(100), nullable=False)
copyright = Column(SmallInteger, nullable=False)
author_id = Column(Integer, ForeignKey('authors.id'))
author = relationship("Author", backref="books")
|
One to One #
Establishing a one-to-one relationship in SQLAlchemy is almost the same as one-to-many relationship, the only difference is that we pass an additional argument uselist=False
to the relationship()
function. Here is an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Person(Base):
__tablename__ = 'persons'
id = Column(Integer(), primary_key=True)
name = Column(String(255), nullable=False)
designation = Column(String(255), nullable=False)
doj = Column(Date(), nullable=False)
dl = relationship('DriverLicense', backref='person', uselist=False)
class DriverLicense(Base):
__tablename__ = 'driverlicense'
id = Column(Integer(), primary_key=True)
license_number = Column(String(255), nullable=False)
renewed_on = Column(Date(), nullable=False)
expiry_date = Column(Date(), nullable=False)
person_id = Column(Integer(), ForeignKey('persons.id')) # Foreign key
|
Given a Person
object p
, p.dl
would return a DriverLicense
object. If we hadn't passed uselist=False
to the relationship()
function then the relationship between Person
and DriverLicense
would be one-to-many and p.dl
would return a list of DriverLicense
objects rather than a single object. The uselist=False
argument doesn't have any effect on the persons
attribute of the DriverLicense
object. As usual, it will return a Person
object.
Many to Many #
Creating a many-to-many relationship requires an extra table called an association table or an intermediary table. We define this table as an instance of the Table
class and then connect it to the model using the secondary
argument of the relationship()
function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | Base = declarative_base()
author_book = Table('author_book', Base.metadata,
Column('author_id', Integer(), ForeignKey("authors.id")),
Column('book_id', Integer(), ForeignKey("books.id"))
)
class Author(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
first_name = Column(String(100), nullable=False)
last_name = Column(String(100), nullable=False)
class Book(Base):
__tablename__ = 'books'
id = Column(Integer, primary_key=True)
title = Column(String(100), nullable=False)
copyright = Column(SmallInteger, nullable=False)
author_id = Column(Integer, ForeignKey('authors.id'))
author = relationship("Author", secondary=author_book, backref="books")
|
An author can write one or more books. Similarly, a book can be written by multiple authors. Thus, there is a many-to-many relationship between author and book.
To represent many-to-many relationship we have created an association table called author_book
.
Given an Author
object a
, we can access all books written by him as a.books
. Similarly, given a Book
object b
, b.authors
will return a list of Author
objects.
In this case, we have defined relationship()
on the Book
model, but we could have just as easily defined it in the Author
model.
There will be times when you want to store additional data in the association table. To achieve this we have to define association table as a model class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | Base = declarative_base()
class Author_Book(Base):
__tablename__ = 'author_book'
author_id = Column(Integer(), ForeignKey("authors.id")),
book_id = Column(Integer(), ForeignKey("books.id"))
extra_data = Column(String(100))
class Author(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
first_name = Column(String(100), nullable=False)
last_name = Column(String(100), nullable=False)
books = relationship("Author_Book", backref='author')
class Book(Base):
__tablename__ = 'books'
id = Column(Integer, primary_key=True)
title = Column(String(100), nullable=False)
copyright = Column(SmallInteger, nullable=False)
authors = relationship("Author_Book", backref="book")
|
Creating Tables #
As in SQLAlchemy Core, use use create_all()
method of the MetaData
instance to create the table.
Base.metadata.create_all(engine)
To drop the tables call drop_all
method.
Base.metadata.drop_all(engine)
We will now re-define the tables using models and persists them to the database by calling the create_all()
method. Here is the complete code to do so:
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | from sqlalchemy import create_engine, MetaData, Table, Integer, String, \
Column, DateTime, ForeignKey, Numeric, SmallInteger
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from datetime import datetime
engine = create_engine("postgres+psycopg2://postgres:pass@localhost/sqlalchemy_tuts")
Base = declarative_base()
class Customer(Base):
__tablename__ = 'customers'
id = Column(Integer(), primary_key=True)
first_name = Column(String(100), nullable=False)
last_name = Column(String(100), nullable=False)
username = Column(String(50), nullable=False)
email = Column(String(200), nullable=False)
created_on = Column(DateTime(), default=datetime.now)
updated_on = Column(DateTime(), default=datetime.now, onupdate=datetime.now)
orders = relationship("Order", backref='customer')
class Item(Base):
__tablename__ = 'items'
id = Column(Integer(), primary_key=True)
name = Column(String(200), nullable=False)
cost_price = Column(Numeric(10, 2), nullable=False)
selling_price = Column(Numeric(10, 2), nullable=False)
# orders = relationship("Order", backref='customer')
class Order(Base):
__tablename__ = 'orders'
id = Column(Integer(), primary_key=True)
customer_id = Column(Integer(), ForeignKey('customers.id'))
date_placed = Column(DateTime(), default=datetime.now)
line_items = relationship("OrderLine", secondary="order_lines", backref='order')
class OrderLine(Base):
__tablename__ = 'order_lines'
id = Column(Integer(), primary_key=True)
order_id = Column(Integer(), ForeignKey('orders.id'))
item_id = Column(Integer(), ForeignKey('items.id'))
quantity = Column(SmallInteger())
item = relationship("Item")
Base.metadata.create_all(engine)
|
In the next chapter, we will learn how to work with the database using SQLAlchemy ORM.
Load Comments