Week 10
Week 10
1. ORM Fundamentals
What is ORM?
Object-Relational Mapping (ORM) is a programming technique that allows
developers to interact with relational databases using object-oriented programming
languages. It acts as a bridge between the object-oriented world of application code
and the relational world of databases.
In traditional database interactions, developers write SQL queries to create, read,
update, and delete data. With ORM, these operations are abstracted away, and
developers can work with database records as if they were regular objects in their
programming language of choice.
Key concepts of ORM include:
1. Mapping: ORM tools provide a way to map database tables to classes in your
programming language. Each row in a table corresponds to an instance of the class,
and each column maps to an attribute of that class.
2. Abstraction: ORM abstracts away the underlying database structure, allowing
developers to work with familiar programming constructs rather than raw SQL.
3. Database Agnostic: Many ORM tools support multiple database systems, allowing
developers to switch between different databases with minimal code changes.
4. Query Language: ORMs often provide their own query language or API that
translates object-oriented operations into SQL queries behind the scenes.
While ORM offers many advantages, it's important to note that it may not be the best
solution for every scenario. In cases where you need fine-grained control over SQL
queries or are working with extremely large datasets, writing raw SQL might be more
appropriate. However, for many applications, the benefits of ORM far outweigh the
drawbacks.
2. SQLAlchemy Overview
SQLAlchemy is a powerful and flexible ORM library for Python. It provides a full
suite of well-known enterprise-level persistence patterns, designed for efficient and
high-performing database access. SQLAlchemy is known for its ability to handle both
simple and complex database operations with equal ease.
Key features of SQLAlchemy include:
1. SQL Expression Language: A SQL construction API that allows for fluent,
Pythonic construction of SQL statements.
2. ORM: An object-relational mapper that provides data mapper patterns for domain
modeling.
3. Schema/Types: Extensible systems for describing database schemas and custom
types.
4. Connection Pooling: Efficient management of database connections.
5. Dialects: Support for multiple database backends through dialects.
SQLAlchemy Core
SQLAlchemy Core is a full-featured SQL abstraction toolkit. It provides a Pythonic
way of representing tables, columns, and SQL expressions. Core is closer to SQL and
gives you more control over the generated queries.
Key aspects of Core:
1. Table and Column Constructs: Define database schema using Python objects.
2. SQL Expression Language: Write SQL-like expressions in Python.
3. Connection and Execution: Direct connection to the database for executing
queries.
4. Result Sets: Fetch and process query results.
# Querying data
with engine.connect() as conn:
result = conn.execute(users.select().where(users.c.name == 'john'))
for row in result:
print(row)
id = Column(Integer, primary_key=True)
name = Column(String)
fullname = Column(String)
engine = create_engine('sqlite:///example.db')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
# Inserting data
new_user = User(name='john', fullname='John Doe')
session.add(new_user)
session.commit()
# Querying data
users = session.query(User).filter_by(name='john').all()
for user in users:
print(user.name, user.fullname)
- Use Core when you need fine-grained control over SQL generation, are working
with existing schemas, or need to optimize for performance.
- Use ORM when you want to work with Python objects, need to manage complex
object relationships, or prefer a higher level of abstraction.
In practice, many applications use a combination of both approaches, leveraging the
strengths of each where appropriate.
3. Setting up SQLAlchemy
Setting up SQLAlchemy involves installing the library and configuring it to work with
your chosen database. We'll cover setup for three popular databases: SQLite,
PostgreSQL, and MySQL.
Installing SQLAlchemy
First, install SQLAlchemy using pip:
pip install sqlalchemy
With SQLite
SQLite is a lightweight, file-based database that's great for development and small
applications. It's included in Python's standard library, so no additional driver is
needed.
python
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
With PostgreSQL
PostgreSQL is a powerful, open-source object-relational database system. To use
SQLAlchemy with PostgreSQL, you'll need to install the psycopg2 driver:
With MySQL
MySQL is another popular open-source relational database. To use SQLAlchemy with
MySQL, you'll need to install the mysqlclient driver:
Base = declarative_base()
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
In each case, the `create_engine()` function is used to create an `Engine` instance. The
`Engine` is the starting point for any SQLAlchemy application. It's the home base for
the actual database and its DBAPI, delivered to the SQLAlchemy application through
a connection pool and a `Dialect`, which describes how to talk to a specific kind of
database/DBAPI combination.
The `declarative_base()` function is used to create a base class for declarative class
definitions. This base class will serve as a foundation for creating your model classes.
The `sessionmaker()` function creates a custom `Session` class which is bound to our
database. The `Session` establishes all conversations with the database and represents
a "holding zone" for all the objects which you've loaded or associated with it during
its lifespan.
After setting up these components, you're ready to start defining your models and
interacting with the database using SQLAlchemy.
id = Column(Integer, primary_key=True)
name = Column(String(50))
fullname = Column(String(50))
email = Column(String(50))
addresses = relationship("Address", back_populates="user")
def __repr__(self):
return f"<User(name='{self.name}', fullname='{self.fullname}',
email='{self.email}')>"
class Address(Base):
__tablename__ = 'addresses'
id = Column(Integer, primary_key=True)
email_address = Column(String(50), nullable=False)
user_id = Column(Integer, ForeignKey('users.id'))
user = relationship("User", back_populates="addresses")
def __repr__(self):
return f"<Address(email_address='{self.email_address}')>"
- Column Types: SQLAlchemy provides many column types like `Integer`, `String`,
`Boolean`, `DateTime`, `Float`, etc. Choose the appropriate type for your data.
- Constraints: You can add constraints to your columns, such as `nullable=False` for
required fields, or `unique=True` for unique values.
- Indexes: You can create indexes on your tables for better query performance using
the `Index` class.
- Table Arguments: You can pass additional arguments to the table constructor using
the `__table_args__` attribute.
By properly defining your models, you create a powerful abstraction layer that allows
you to work with your database using Python objects, greatly simplifying database
operations in your application.
Read
SQLAlchemy provides several ways to query the database:
Update
To update a record, you first query for the object, modify its attributes, and then
commit the session:
user = session.query(User).filter_by(name='alice').first()
user.email = '[email protected]'
session.commit()
You can also update multiple records at once:
session.query(User).filter(User.name.like('a%')).update({"fullname": "Updated
Name"}, synchronize_session=False)
session.commit()
Delete
To delete a record, you can either delete a specific object or delete based on a query:
1. Delete a specific object:
python
user = session.query(User).filter_by(name='alice').first()
session.delete(user)
session.commit()
6. Relationships in SQLAlchemy
Relationships are a key feature of relational databases, and SQLAlchemy provides
powerful tools for working with them. There are three main types of relationships:
One-to-One, One-to-Many, and Many-to-Many. Let's explore each type using
examples.
One-to-One Relationships
A one-to-one relationship exists when one record in a table is associated with one and
only one record in another table. For example, let's consider a `User` and a `Profile`
where each user has exactly one profile.
python
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
profile = relationship("Profile", uselist=False, back_populates="user")
class Profile(Base):
__tablename__ = 'profiles'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'), unique=True)
bio = Column(String(200))
user = relationship("User", back_populates="profile")
In this example:
- `uselist=False` in the `User` class ensures that `profile` is treated as a scalar (single
object) rather than a list.
- `unique=True` on `Profile.user_id` ensures that each user can have only one profile.
Usage:
python
user = User(name="John Doe")
profile = Profile(bio="A software developer")
user.profile = profile
session.add(user)
session.commit()
# Accessing the relationship
print(user.profile.bio) # Output: A software developer
print(profile.user.name) # Output: John Doe
One-to-Many Relationships
A one-to-many relationship exists when a record in one table can be associated with
multiple records in another table. For example, a `User` can have multiple `Post`s.
python
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
posts = relationship("Post", back_populates="author")
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
title = Column(String(100))
content = Column(String(500))
author_id = Column(Integer, ForeignKey('users.id'))
author = relationship("User", back_populates="posts")
Usage:
python
user = User(name="Jane Smith")
post1 = Post(title="First Post", content="Hello, world!")
post2 = Post(title="Second Post", content="Another post")
user.posts.extend([post1, post2])
session.add(user)
session.commit()
# Accessing the relationship
for post in user.posts:
print(f"{post.title} by {post.author.name}")
Many-to-Many Relationships
A many-to-many relationship exists when multiple records in one table are associated
with multiple records in another table. This typically involves a third, intermediate
table. For example, let's consider `Student`s and `Course`s, where a student can enroll
in multiple courses and a course can have multiple students.
python
student_course = Table('student_course', Base.metadata,
Column('student_id', Integer, ForeignKey('students.id')),
Column('course_id', Integer, ForeignKey('courses.id'))
)
class Student(Base):
__tablename__ = 'students'
id = Column(Integer, primary_key=True)
name = Column(String(50))
courses = relationship("Course", secondary=student_course,
back_populates="students")
class Course(Base):
__tablename__ = 'courses'
id = Column(Integer, primary_key=True)
name = Column(String(50))
students = relationship("Student", secondary=student_course,
back_populates="courses")
In this example, `student_course` is the association table that links students and
courses.
Usage:
python
student1 = Student(name="Alice")
student2 = Student(name="Bob")
course1 = Course(name="Mathematics")
course2 = Course(name="Physics")
student1.courses.extend([course1, course2])
student2.courses.append(course1)
session.add_all([student1, student2, course1, course2])
session.commit()
# Accessing the relationship
for student in course1.students:
print(f"{student.name} is enrolled in {course1.name}")
Eager Loading
When working with relationships, it's important to be aware of the "N+1 problem,"
where accessing related objects can result in multiple database queries. SQLAlchemy
provides eager loading strategies to mitigate this:
1. Joined Loading: Loads the related objects in the same query using JOIN.
users = session.query(User).options(joinedload(User.posts)).all()
2. Subquery Loading: Loads the related objects using a separate query.
users = session.query(User).options(subqueryload(User.posts)).all()
3. Selectin Loading: Similar to subquery loading but uses IN clauses.
users = session.query(User).options(selectinload(User.posts)).all()
Choose the appropriate loading strategy based on your specific use case and
performance requirements.