Модели с отношением один-ко-многим

Последнее обновление: 02.02.2023

Отношение один-ко-многим (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()
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850