From 20e426c01bb9bc463e842c69558e55c4b4e54e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7oise=20Conil?= <francoise.conil@liris.cnrs.fr> Date: Mon, 3 Jan 2022 16:50:25 +0100 Subject: [PATCH] SQLAlchemy : basic relationship patterns --- relationship/association_object.py | 106 ++++++++++++++++++++++ relationship/basic-relationship.py | 73 +++++++++++++++ relationship/many_to_many.delete_pb.py | 91 +++++++++++++++++++ relationship/many_to_many.py | 119 +++++++++++++++++++++++++ relationship/many_to_one.py | 41 +++++++++ relationship/one_to_many.1.py | 35 ++++++++ relationship/one_to_many.2.py | 41 +++++++++ relationship/one_to_one.py | 75 ++++++++++++++++ 8 files changed, 581 insertions(+) create mode 100644 relationship/association_object.py create mode 100644 relationship/basic-relationship.py create mode 100644 relationship/many_to_many.delete_pb.py create mode 100644 relationship/many_to_many.py create mode 100644 relationship/many_to_one.py create mode 100644 relationship/one_to_many.1.py create mode 100644 relationship/one_to_many.2.py create mode 100644 relationship/one_to_one.py diff --git a/relationship/association_object.py b/relationship/association_object.py new file mode 100644 index 0000000..da637d2 --- /dev/null +++ b/relationship/association_object.py @@ -0,0 +1,106 @@ +""" +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) diff --git a/relationship/basic-relationship.py b/relationship/basic-relationship.py new file mode 100644 index 0000000..acbfb9d --- /dev/null +++ b/relationship/basic-relationship.py @@ -0,0 +1,73 @@ +""" +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) diff --git a/relationship/many_to_many.delete_pb.py b/relationship/many_to_many.delete_pb.py new file mode 100644 index 0000000..79cbc07 --- /dev/null +++ b/relationship/many_to_many.delete_pb.py @@ -0,0 +1,91 @@ +""" +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) diff --git a/relationship/many_to_many.py b/relationship/many_to_many.py new file mode 100644 index 0000000..6cdca4b --- /dev/null +++ b/relationship/many_to_many.py @@ -0,0 +1,119 @@ +""" +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) diff --git a/relationship/many_to_one.py b/relationship/many_to_one.py new file mode 100644 index 0000000..3196944 --- /dev/null +++ b/relationship/many_to_one.py @@ -0,0 +1,41 @@ +""" +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 diff --git a/relationship/one_to_many.1.py b/relationship/one_to_many.1.py new file mode 100644 index 0000000..4215fbd --- /dev/null +++ b/relationship/one_to_many.1.py @@ -0,0 +1,35 @@ +""" +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 diff --git a/relationship/one_to_many.2.py b/relationship/one_to_many.2.py new file mode 100644 index 0000000..e9855e9 --- /dev/null +++ b/relationship/one_to_many.2.py @@ -0,0 +1,41 @@ +""" +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 diff --git a/relationship/one_to_one.py b/relationship/one_to_one.py new file mode 100644 index 0000000..1b62262 --- /dev/null +++ b/relationship/one_to_one.py @@ -0,0 +1,75 @@ +""" +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) -- GitLab