Отношение один-ко-многим (one-to-many) представляет ситуацию, когда одна модель хранит ссылку на один объект другой модели, а вторая модель может ссылаться на коллекцию объектов первой модели. Например, в одной компании может работать несколько пользователей, а каждый пользователь в свою очередь может официально работать только в одной компании:
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
sqlite_database = "sqlite:///metanit2.db"
engine = create_engine(sqlite_database)
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
company_id = Column(Integer, ForeignKey("companies.id"))
company = relationship("Company", back_populates="users")
class Company(Base):
__tablename__ = "companies"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
users = relationship("User", back_populates="company")
Base.metadata.create_all(bind=engine)
Здесь пользователи представлены моделью User, а компании - моделью Company. Оба класса имеют обычные атрибуты-столбцы - id и name. Но кроме того, они имеют атрибуты, которые позволяют установить отношения между моделями
#class User
company_id = Column(Integer, ForeignKey("companies.id"))
company = relationship("Company", back_populates="users")
#class Company
users = relationship("User", back_populates="company")
Для установки отношений между моделями применяется функция relationship(). Она принимает множество параметров, из которых самый первый параметр указывает на связанную модель. А параметр back_populates представляет атрибут связанной модели, с которой будет сопоставляться текущая модель. Например, в классе Company атрибут
users = relationship("User", back_populates="company")
указывает, что он будет связан с моделью User через ее атрибут "company".
В классе User мы имеем обратную ситуацию
company = relationship("Company", back_populates="users")
здесь атрибут company связан с моделью Company через ее атрибут "users". То есть получается связь User.company -- Company.users.
Но какая из этих моделей будет главной и хранить список объектов, а какая будет подчиненной и хранить ссылку на один объект связанной модели? Для этого в подчиненной модели User определяем атрибут-столбец, который будет представлять внешний ключ:
company_id = Column(Integer, ForeignKey("companies.id"))
То есть атрибут company_id будет представлять числовой внешний ключ на столбец id из таблицы "companies".
После выполнения программы в базе данных metanit2.db будут созданы две таблицы с помощью следующих скриптов SQL:
CREATE TABLE companies (
id INTEGER NOT NULL,
name VARCHAR,
PRIMARY KEY (id)
)
CREATE TABLE users (
id INTEGER NOT NULL,
name VARCHAR,
company_id INTEGER,
PRIMARY KEY (id),
FOREIGN KEY(company_id) REFERENCES companies (id)
)
Для добавления данных моделей, связанных отношением один ко многим, используются уже ранее рассмотренные методы добавления в бд. При этом, при добавлении одного объекта все связанные с ним объекты добавляются автоматически:
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship, Session
sqlite_database = "sqlite:///metanit2.db"
engine = create_engine(sqlite_database)
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
company_id = Column(Integer, ForeignKey("companies.id"))
company = relationship("Company", back_populates="users")
class Company(Base):
__tablename__ = "companies"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
users = relationship("User", back_populates="company")
with Session(autoflush=False, bind=engine) as db:
# создаем компании
microsoft = Company(name="Microsoft")
google = Company(name="Google")
# создаем пользователей
tom = User(name="Tom")
bob = User(name="Bob")
# устанавливаем для компаний списки пользователей
microsoft.users=[tom]
google.users = [bob]
# добавляем компании в базу данных, и вместе с ними добавляются пользователи
db.add_all([microsoft, google])
db.commit()
# можно отдельно добавить объект в список
alice = User(name="Alice")
google.users.extend([alice]) # добавляем список из одного элемента
# можно установить для пользователя определенную компанию
sam = User(name="Sam")
sam.company = microsoft
db.add(sam)
db.commit()
При добавлении компаний в бд также добавляются связанные с ними пользователи(если они не добавлены в бд)
db.add_all([microsoft, google])
Также можно отдельно добавлять пользователей в определенную компанию, используя методы списков:
google.users.extend([alice])
Также можно, наоборот, у пользователя установить компанию:
sam.company = microsoft
Через атрибуты, через которые установлена связь между моделями, можно получить связанные данные. Например, получим компании пользователей:
with Session(autoflush=False, bind=engine) as db:
# получение всех объектов
users = db.query(User).all()
for u in users:
print(f"{u.name} ({u.company.name})")
Консольный вывод
Tom (Microsoft) Bob (Google) Alice (Google) Sam (Microsoft)
Получение пользователей у компаний:
with Session(autoflush=False, bind=engine) as db:
# получение всех объектов
companies = db.query(Company).all()
for c in companies:
print(f"{c.name}")
for u in c.users: print(f"{u.name}")
print()
Консольный вывод
Microsoft Tom Sam Google Bob Alice
Редактирование производится как и в общем случае. Например, изменим у пользователя компанию:
with Session(autoflush=False, bind=engine) as db:
# получаем пользователя с именем Tom
tom = db.query(User).filter(User.name=="Tom").first()
# получаем компанию Google
google = db.query(Company).filter(Company.name=="Google").first()
# меняем у Тома компанию на Google
if tom != None and google !=None:
tom.company = google
db.commit()
# проверяем изменение
users = db.query(User).all()
for u in users:
print(f"{u.name} - {u.company.name}")
Консольный вывод
Tom - Google Bob - Google Alice - Google Sam - Microsoft
Для удаления объекта зависимой модели из списка объектов в главной модели, можно использовать методы списка, в частности, метод remove():
with Session(autoflush=False, bind=engine) as db:
# получаем пользователя с именем Tom
tom = db.query(User).filter(User.name=="Tom").first()
# получаем компанию Google
google = db.query(Company).filter(Company.name=="Google").first()
# удаляем Тома из компании Google
if tom != None and google !=None:
google.users.remove(tom)
db.commit()
# проверяем изменение
users = db.query(User).all()
for u in users:
print(f"{u.name} - {u.company.name if u.company is not None else None}")
Консольный вывод
Tom - None Bob - Google Alice - Google Sam - Microsoft
Удаление объекта зависимой модели (User) из базы данных проиходит как и в общем случае.
with Session(autoflush=False, bind=engine) as db:
# получаем пользователя с именем Tom
tom = db.query(User).filter(User.name=="Tom").first()
# удаляем Toma
db.delete(tom)
db.commit()
Удаление объекта главной модели (Company) из базы данных зависит от настройки выражения ON DELETE. Например, для выше определенных моделей User и Company
отношение установливалось следующим образом:
# User
company_id = Column(Integer, ForeignKey("companies.id"))
company = relationship("Company", back_populates="users")
#class Company
users = relationship("User", back_populates="company")
В данном случае атрибут company_id в модели User может принимать значение None (на уровне базы данных столбец может принимать значение NULL). При удалении объекта главной модели, этот столбец company_id получит значение NULL (то есть компания для пользователя не установлена)
with Session(autoflush=False, bind=engine) as db:
# получаем компанию Google
google = db.query(Company).filter(Company.name=="Google").first()
# удаляем ее
db.delete(google)
db.commit()
Однако нередко применяется каскадное удаление, при котором при удалении объекта главной модели также удаляются все связанные с ней объекты зависимой модели.
Для установки каскадного удаления в функции relationship() применяется параметр cascade, которая получает значение "all, delete-orphan".
Например, создадим новую базу данных с подобной настройкой:
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship, Session
sqlite_database = "sqlite:///metanit3.db"
engine = create_engine(sqlite_database)
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
company_id = Column(Integer, ForeignKey("companies.id"))
company = relationship("Company", back_populates="users")
class Company(Base):
__tablename__ = "companies"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
users = relationship("User", back_populates="company", cascade="all, delete-orphan")
Base.metadata.create_all(bind=engine)
with Session(autoflush=False, bind=engine) as db:
# создаем для теста компанию
google = Company(name="Google")
# создаем пользователей
tom = User(name="Tom")
bob = User(name="Bob")
# устанавливаем для компаний список пользователей
google.users=[tom, bob]
db.add(google)
db.commit()
Ключевой момент здесь - установка атрибута users в классе Company:
users = relationship("User", back_populates="company", cascade="all, delete-orphan")
Удалим компанию, и вместе с ней будут удалены все связанные с ней пользователи:
with Session(autoflush=False, bind=engine) as db:
# получаем компанию Google
google = db.query(Company).filter(Company.name=="Google").first()
# удаляем ее
db.delete(google)
db.commit()