Skip to content
Snippets Groups Projects
Commit 20e426c0 authored by Françoise Conil's avatar Françoise Conil
Browse files

SQLAlchemy : basic relationship patterns

parent fdbdd01b
No related branches found
No related tags found
No related merge requests found
"""
https://docs.sqlalchemy.org/en/14/orm/basic_relationships.html#association-object
The association object pattern is a variant on many-to-many: it’s used when
your association table contains additional columns beyond those which are
foreign keys to the left and right tables. Instead of using the
relationship.secondary argument, you map a new class directly to the
association table. The left side of the relationship references the association
object via one-to-many, and the association class references the right side
via many-to-one.
As always, the bidirectional version makes use of relationship.back_populates
or relationship.backref.
WARNING : A EXPLICITER
The association object pattern does not coordinate changes with a separate
relationship that maps the association table as “secondary”.
"""
from sqlalchemy import create_engine
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm.session import Session
Base = declarative_base()
class ParentChild(Base):
__tablename__ = "parent_child"
parent_id = Column(
ForeignKey("parent.id", ondelete="CASCADE"), primary_key=True
)
child_id = Column(
ForeignKey("child.id", ondelete="CASCADE"), primary_key=True
)
# First child, ...
rank = Column(Integer)
# and the association class references the right side via many-to-one
# As always, the bidirectional version makes use of
# relationship.back_populates or relationship.backref
child = relationship("Child", back_populates="parents")
parent = relationship("Parent", back_populates="children")
class Parent(Base):
__tablename__ = "parent"
id = Column(Integer, primary_key=True)
name = Column(String)
# The left side of the relationship references the association object via
# one-to-many
children = relationship("ParentChild", back_populates="parent")
def __repr__(self):
return f"<Parent (name={self.name}, children={self.children})>"
class Child(Base):
__tablename__ = "child"
id = Column(Integer, primary_key=True)
name = Column(String)
parents = relationship("ParentChild", back_populates="child")
def __repr__(self):
return f"<Child (name={self.name})>"
if __name__ == "__main__":
engine = create_engine("sqlite:///association_object.db", echo=False)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
# cf ../basic-session.1.4.py
with Session(engine) as session:
# Working with the association pattern in its direct form requires that
# child objects are associated with an association instance before
# being appended to the parent; similarly, access from parent to child
# goes through the association object
# create parent, append a child via association
jack = Parent(name="Jack")
john = Child(name="John")
pc = ParentChild(rank=1)
pc.child = john
jack.children.append(pc)
with session.begin():
session.add(jack)
session.add(pc)
# iterate through child objects via association, including association
# attributes
for assoc in jack.children:
print(f"{assoc.rank=}")
print(f"{assoc.child=}")
print(jack)
"""
https://docs.sqlalchemy.org/en/14/orm/tutorial.html#building-a-relationship
https://docs.sqlalchemy.org/en/14/orm/basic_relationships.html
The two complementing relationships Address.user and User.addresses are referred
to as a bidirectional relationship, and is a key feature of the SQLAlchemy ORM.
"""
from sqlalchemy import create_engine
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm.session import Session
Base = declarative_base()
class Address(Base):
__tablename__ = "addresses"
id = Column(Integer, primary_key=True)
email_address = Column(String, nullable=True)
user_id = Column(Integer, ForeignKey("users.id"))
user = relationship("User", back_populates="addresses")
def __repr__(self):
return (
f"<Address (email_address={self.email_address}, user={self.user})>"
)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
fullname = Column(String)
nickname = Column(String)
addresses = relationship(
"Address", order_by="Address.id", back_populates="user"
)
def __repr__(self):
return (
f"<User (name={self.name}, fullname={self.fullname},"
f"nickname={self.nickname}, addresses={self.addresses})>"
)
if __name__ == "__main__":
engine = create_engine("sqlite:///building_a_relationship.db", echo=False)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
# cf ../basic-session.1.4.py
with Session(engine) as session:
with session.begin():
ed_user = User(
name="ed", fullname="Ed Jones", nickname="edsnickname"
)
session.add(ed_user)
jack = User(name="jack", fullname="Jack Bean", nickname="gjffdd")
addr1 = Address(email_address="jack@google.com")
addr2 = Address(email_address="j25@yahoo.com")
jack.addresses = [addr1, addr2]
session.add(jack)
print(jack)
print(addr1)
"""
https://docs.sqlalchemy.org/en/14/orm/basic_relationships.html
Many to Many adds an association table between two classes. The association
table is indicated by the relationship.secondary argument to relationship().
Usually, the Table uses the MetaData object associated with the declarative
base class, so that the ForeignKey directives can locate the remote tables
with which to link.
It is also recommended, though not in any way required by SQLAlchemy, that
the columns which refer to the two entity tables are established within either
a unique constraint or more commonly as the primary key constraint; this
ensures that duplicate rows won’t be persisted within the table regardless of
issues on the application side.
"""
from sqlalchemy import create_engine
from sqlalchemy import Column, ForeignKey, Integer, String, Table
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm.session import Session
Base = declarative_base()
parent_child_table = Table(
"parent_child",
Base.metadata,
# Create a composed primary key : PRIMARY KEY (parent_id, child_id)
Column("parent_id", ForeignKey("parent.id"), primary_key=True),
Column("child_id", ForeignKey("child.id"), primary_key=True),
)
class Parent(Base):
__tablename__ = "parent"
id = Column(Integer, primary_key=True)
name = Column(String)
children = relationship(
"Child", secondary=parent_child_table, back_populates="parents"
)
def __repr__(self):
return f"<Parent (name={self.name}, children={self.children})>" # , children={self.children})>" # noqa E501
class Child(Base):
__tablename__ = "child"
id = Column(Integer, primary_key=True)
name = Column(String)
parents = relationship(
"Parent", secondary=parent_child_table, back_populates="children"
)
def __repr__(self):
return f"<Child (name={self.name})>" # , parents={self.parents})>"
if __name__ == "__main__":
engine = create_engine("sqlite:///many_to_many.delete_pb.db", echo=False)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
# cf ../basic-session.1.4.py
with Session(engine) as session:
jack = Parent(name="Jack")
wendy = Parent(name="Wendy")
jim = Parent(name="Jim")
john = Child(name="John")
alice = Child(name="Alice")
john.parents = [jack, wendy]
alice.parents = [wendy, jim]
with session.begin():
session.add(jack)
session.add(wendy)
session.add(jim)
session.add(john)
session.add(alice)
with session.begin():
# wendy.children.remove(john)
session.delete(alice)
print(wendy)
print(john)
"""
https://docs.sqlalchemy.org/en/14/orm/basic_relationships.html
Many to Many adds an association table between two classes. The association
table is indicated by the relationship.secondary argument to relationship().
Usually, the Table uses the MetaData object associated with the declarative
base class, so that the ForeignKey directives can locate the remote tables
with which to link.
It is also recommended, though not in any way required by SQLAlchemy, that
the columns which refer to the two entity tables are established within either
a unique constraint or more commonly as the primary key constraint; this
ensures that duplicate rows won’t be persisted within the table regardless of
issues on the application side.
DELETE PB
https://docs.sqlalchemy.org/en/14/orm/basic_relationships.html#deleting-rows-from-the-many-to-many-table
https://docs.sqlalchemy.org/en/14/orm/cascades.html#passive-deletes
https://docs.sqlalchemy.org/en/14/orm/relationship_api.html
In order to use ON DELETE foreign key cascades in conjunction with
relationship(), it’s important to note first and foremost that the
relationship.cascade setting must still be configured to match the
desired “delete” or “set null” behavior (using delete cascade or
leaving it omitted).
Le comportement du DELETE est plus clair dans la page suivante :
https://docs.sqlalchemy.org/en/14/orm/session_basics.html#deleting
"""
from sqlalchemy import create_engine
from sqlalchemy import Column, ForeignKey, Integer, String, Table
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm.session import Session
Base = declarative_base()
parent_child_table = Table(
"parent_child",
Base.metadata,
# Create a composed primary key : PRIMARY KEY (parent_id, child_id)
Column(
"parent_id",
ForeignKey("parent.id", ondelete="CASCADE"),
primary_key=True,
),
Column(
"child_id", ForeignKey("child.id", ondelete="CASCADE"), primary_key=True
),
)
class Parent(Base):
__tablename__ = "parent"
id = Column(Integer, primary_key=True)
name = Column(String)
children = relationship(
"Child",
secondary=parent_child_table,
back_populates="parents",
cascade="all, delete",
)
def __repr__(self):
return f"<Parent (name={self.name}, children={self.children})>" # , children={self.children})>" # noqa E501
class Child(Base):
__tablename__ = "child"
id = Column(Integer, primary_key=True)
name = Column(String)
parents = relationship(
"Parent",
secondary=parent_child_table,
back_populates="children",
passive_deletes=True,
)
def __repr__(self):
return f"<Child (name={self.name})>" # , parents={self.parents})>"
if __name__ == "__main__":
engine = create_engine("sqlite:///many_to_many.db", echo=False)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
# cf ../basic-session.1.4.py
with Session(engine) as session:
jack = Parent(name="Jack")
wendy = Parent(name="Wendy")
jim = Parent(name="Jim")
john = Child(name="John")
alice = Child(name="Alice")
john.parents = [jack, wendy]
alice.parents = [wendy, jim]
with session.begin():
session.add(jack)
session.add(wendy)
session.add(jim)
session.add(john)
session.add(alice)
with session.begin():
# wendy.children.remove(john)
session.delete(alice)
print(wendy)
print(john)
"""
https://docs.sqlalchemy.org/en/14/orm/basic_relationships.html
Je n'arrive pas à comprendre la différence avec l'exemple one-to-many
puisque dans ce cas on semble avoir plusieurs parents associés à un enfant.
"""
from sqlalchemy import create_engine
from sqlalchemy import Column, ForeignKey, Integer
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm.session import Session
Base = declarative_base()
class Parent(Base):
__tablename__ = "parent"
id = Column(Integer, primary_key=True)
child_id = Column(Integer, ForeignKey("child.id"))
child = relationship("Child", back_populates="parents")
class Child(Base):
__tablename__ = "child"
id = Column(Integer, primary_key=True)
parents = relationship("Parent", back_populates="child")
if __name__ == "__main__":
engine = create_engine("sqlite:///many_to_one.db", echo=False)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
# cf ../basic-session.1.4.py
with Session(engine) as session:
with session.begin():
pass
"""
https://docs.sqlalchemy.org/en/14/orm/basic_relationships.html
"""
from sqlalchemy import create_engine
from sqlalchemy import Column, ForeignKey, Integer
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm.session import Session
Base = declarative_base()
class Parent(Base):
__tablename__ = "parent"
id = Column(Integer, primary_key=True)
children = relationship("Child")
class Child(Base):
__tablename__ = "child"
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey("parent.id"))
if __name__ == "__main__":
engine = create_engine("sqlite:///one_to_many.1.db", echo=False)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
# cf ../basic-session.1.4.py
with Session(engine) as session:
with session.begin():
pass
"""
https://docs.sqlalchemy.org/en/14/orm/basic_relationships.html
To establish a bidirectional relationship in one-to-many, where the “reverse” side is a
many to one, specify an additional relationship() and connect the two using the
relationship.back_populates parameter.
"""
from sqlalchemy import create_engine
from sqlalchemy import Column, ForeignKey, Integer
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm.session import Session
Base = declarative_base()
class Parent(Base):
__tablename__ = "parent"
id = Column(Integer, primary_key=True)
children = relationship("Child", back_populates="parent")
class Child(Base):
__tablename__ = "child"
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey("parent.id"))
parent = relationship("Parent", back_populates="children")
if __name__ == "__main__":
engine = create_engine("sqlite:///one_to_many.2.db", echo=False)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
# cf ../basic-session.1.4.py
with Session(engine) as session:
with session.begin():
pass
"""
https://docs.sqlalchemy.org/en/14/orm/basic_relationships.html
Pourquoi ne pas utiliser ce que Nicolas m'a indiqué avec "unique" ?
=> Si c'est évoqué dans le 3ème paragraphe
The “one-to-one” convention is achieved by applying a value of False
to the relationship.uselist parameter of the relationship() construct,
As mentioned previously, the ORM considers the “one-to-one” pattern as
a convention, where it makes the assumption that when it loads the
Parent.child attribute on a Parent object, it will get only one row back.
If more than one row is returned, the ORM will emit a warning.
However, the Child.parent side of the above relationship remains as a
“many-to-one” relationship and is unchanged, and there is no intrinsic
system within the ORM itself that prevents more than one Child object
to be created against the same Parent during persistence.
Instead, techniques such as unique constraints may be used in the actual
database schema to enforce this arrangement, where a unique constraint on
the Child.parent_id column would ensure that only one Child row may refer
to a particular Parent row at a time.
"""
from sqlalchemy import create_engine
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm.session import Session
Base = declarative_base()
class Parent(Base):
__tablename__ = "parent"
id = Column(Integer, primary_key=True)
name = Column(String)
child = relationship("Child", back_populates="parent", uselist=False)
def __repr__(self):
return f"<Parent (name={self.name})>"
class Child(Base):
__tablename__ = "child"
id = Column(Integer, primary_key=True)
name = Column(String)
# TODO : ajouter une contrainte "unique" sur la colonne
parent_id = Column(Integer, ForeignKey("parent.id"), unique=True)
parent = relationship("Parent", back_populates="child")
def __repr__(self):
return f"<Child (name={self.name}, parent={self.parent})>"
if __name__ == "__main__":
engine = create_engine("sqlite:///one_to_one.db", echo=False)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
# cf ../basic-session.1.4.py
with Session(engine) as session:
with session.begin():
jack = Parent(name="Jack")
session.add(jack)
john = Child(name="John")
john.parent = jack
session.add(john)
print(jack)
print(john)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment