Vous êtes ici : Accueil Tutoriels SQLAlchemy Relations et dépendances

Relations et dépendances

La gestion des relations entre entités SQL est un point important qui n'est pas trivial. Voici quelques bonnes pratiques.

Relations

Pour ce tutoriel, on va reprendre l'exemple Utilisateur / groupe en partant de ces classes :

class Group(Base):
__tablename__ = 'auth_group'
id = Column(Integer, autoincrement=True, primary_key=True)
name = Column(Unicode(16), unique=True, nullable=False)
display_name = Column(Unicode(255))
created = Column(DateTime, default=datetime.now)


class User(Base):
__tablename__ = 'auth_user'
id = Column(Integer, autoincrement=True, primary_key=True)
name = Column(Unicode(16), unique=True, nullable=False)
email_address = Column(Unicode(255), unique=True, nullable=False, info={'rum': {'field':'Email'}})
display_name = Column(Unicode(255))
_password = Column('password', Unicode(128), info={'rum': {'field':'Password'}})
created = Column(DateTime, default=datetime.now)

Relation plusieurs à plusieurs

L'idée est de définir les relations entre groupe et utilisateur. Chaque utilisateur peut appartenir à 0 à n groupes et chaque groupe peut contenir 0 à n utilisateurs. En SQL, il faut passer par ce que l'on appelle une table d'association. L'ORM ne réinvente pas la roue et passe lui aussi par cette étape. Puis, dans le corps de l'une des deux classes, on définit la relation elle-même :

user_group_table = Table('auth_user_group', metadata,
Column('user_id', Integer, ForeignKey('auth_user.id',
onupdate="CASCADE", ondelete="CASCADE"), primary_key=True),
Column('group_id', Integer, ForeignKey('auth_group.id',
onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
)


class Group(Base):
__tablename__ = 'auth_group'
id = Column(Integer, autoincrement=True, primary_key=True)
name = Column(Unicode(16), unique=True, nullable=False)
display_name = Column(Unicode(255))
created = Column(DateTime, default=datetime.now)

users = relation('User', secondary=user_group_table, backref='groups')


class User(Base):
__tablename__ = 'auth_user'
id = Column(Integer, autoincrement=True, primary_key=True)
name = Column(Unicode(16), unique=True, nullable=False)
email_address = Column(Unicode(255), unique=True, nullable=False, info={'rum': {'field':'Email'}})
display_name = Column(Unicode(255))
_password = Column('password', Unicode(128), info={'rum': {'field':'Password'}})
created = Column(DateTime, default=datetime.now)

La relation est définie dans une des deux classes, mais est valable dans les deux sens :

>>> g = DBSession.query(Group).first()
>>> g.users
[<User: name=u4, email=u4@inspyration.org, display=user 4>, <User: name=u3, email=u3@inspyration.org, display=user 3>, <User: name=u2, email=u2@inspyration.org, display=user 2>, <User: name=u5, email=u5@inspyration.org, display=user 5>, <User: name=u1, email=u1@inspyration.org, display=user 1>]
>>> g.users[0].groups
[<Group: name=g1>, <Group: name=g3>]

Relation plusieurs pour un

Imaginons que nous voulions désigner un et un seul responsable pour un groupe.

Pour déclarer une relation plusieurs pour un, il faut procéder en deux étapes, dans le corps de la classe groupe :

  • déclaration du champs contenant la clé étrangère :
    id_owner = Column(Integer, ForeignKey('auth_user.id'))
  • déclaration du champs de la relation :
    owner = relation('User')

Au final, un groupe ne pourra avoir qu'un seul utilisateur "responsable", mais aucune contrainte ne pèse sur l'utilisateur qui pourra être responsable de 0 à n groupes.

class Group(Base):
__tablename__ = 'auth_group'
id = Column(Integer, autoincrement=True, primary_key=True)
id_owner = Column(Integer, ForeignKey('auth_user.id'))
name = Column(Unicode(16), unique=True, nullable=False)
display_name = Column(Unicode(255))
created = Column(DateTime, default=datetime.now)

owner = relation('User', backref='managed_groups')


class User(Base):
__tablename__ = 'auth_user'
id = Column(Integer, autoincrement=True, primary_key=True)
name = Column(Unicode(16), unique=True, nullable=False)
email_address = Column(Unicode(255), unique=True, nullable=False, info={'rum': {'field':'Email'}})
display_name = Column(Unicode(255))
_password = Column('password', Unicode(128), info={'rum': {'field':'Password'}})
created = Column(DateTime, default=datetime.now)

Le fait de ne modifier que la classe groupe a son importance. C'est au niveau du groupe que l'on gère son responsable. C'est donc au niveau du groupe que l'on va définir la clé étrangère et la relation. Grâce à la référence de retour, l'objet utilisateur peut également accéder à l'ensemble des groupes qu'il gère.

Relation plusieurs pour un

Ici, les choses sont sensiblement différentes. L'accent est porté sur la relation de l'utilisateur vers le groupe plutôt que du groupe vers l'utilisateur. On définit la même relation mais de manière différente.

class Group(Base):
 __tablename__ = 'auth_group'
 id = Column(Integer, autoincrement=True, primary_key=True)
 id_owner = Column(Integer, ForeignKey('auth_user.id'))
 name = Column(Unicode(16), unique=True, nullable=False)
 display_name = Column(Unicode(255))
 created = Column(DateTime, default=datetime.now)

class User(Base):
 __tablename__ = 'auth_user'
 id = Column(Integer, autoincrement=True, primary_key=True)
 name = Column(Unicode(16), unique=True, nullable=False)
 email_address = Column(Unicode(255), unique=True, nullable=False, info={'rum': {'field':'Email'}})
 display_name = Column(Unicode(255))
 _password = Column('password', Unicode(128), info={'rum': {'field':'Password'}})
 created = Column(DateTime, default=datetime.now)
 managed_groups = relation('User', backref='owner')

La différence avec l'exemple précédent tient au point de vue. De plus, les références retour ne sont pas obligatoires, elles ne sont utiles que si l'on souhaite pouvoir accéder aux données dans les deux sens. Par contre, si on ne les mets pas, il y a alors une vraie différence entre la définition de la relation plusieurs pour un et un pour plusieurs.

Relation un à un

Imaginons qu'à chaque utilisateur, nous adjoignions un profil, dans une table séparés. L'utilisateur serait un composant purement technique, utilisé pour la connexion à l'application, tandis que le profil contiendrait des informations personnelles, utiles pour certains types d'utilisateurs.

Chaque utilisateur a donc entre 0 et un profil et chaque profil appartient à un et un seul utilisateur. Voici comment implémenter cela :

class User(Base):
__tablename__ = 'auth_user'
id = Column(Integer, autoincrement=True, primary_key=True)
name = Column(Unicode(16), unique=True, nullable=False)
email_address = Column(Unicode(255), unique=True, nullable=False, info={'rum': {'field':'Email'}})
display_name = Column(Unicode(255))
_password = Column('password', Unicode(128), info={'rum': {'field':'Password'}})
created = Column(DateTime, default=datetime.now)
profile = relation('Profile', uselist=False, backref='user')

class Profile(Base):
__tablename__ = 'profile'
id_user = Column(Integer, ForeignKey('authuser.id'), primary_key=True, autoincrement=False)
# Autres champs contenant les données

La déclaration de la clé étrangère explicite la relation un à plusieurs, mais c'est l'attribut uselist permet d'expliciter le fait que la relation est en réalité un pour un.

A noter qu'il eut été possible de faire autrement en écrivant à la place ceci :

class User(Base):
__tablename__ = 'auth_user'
id = Column(Integer, autoincrement=True, primary_key=True)
name = Column(Unicode(16), unique=True, nullable=False)
email_address = Column(Unicode(255), unique=True, nullable=False, info={'rum': {'field':'Email'}})
display_name = Column(Unicode(255))
_password = Column('password', Unicode(128), info={'rum': {'field':'Password'}})
created = Column(DateTime, default=datetime.now)

class Profile(Base):
__tablename__ = 'profile'
id_user = Column(Integer, ForeignKey('authuser.id'), primary_key=True, autoincrement=False)
# Autres champs contenant les données
user = relation('User', uselist=False, backref=backref('profile', uselist=False))

Là encore, le résultat final est le même, mais il y a une différence si l'attribut backref n'est pas utilisé.

Par contre, cette seconde méthode a l'avantage de rajouter des fonctionnalités à l'objet User sans toucher à son code, ce qui est avantageux pour plusieurs raisons, en particulier pour des raisons de modularité du code.

Gestion des dépendances

Comme on l'a vu, le fait d'utiliser la chaîne de caractères 'User' à la place de l'objet User permet de faire nos déclarations alors que les objets ne sont pas encore déclarés et donc de résoudre les problèmes de dépendance cyclique.

Mais cette astuce ne résout pas tout. En effet, il existe des situations où cela ne suffit pas, en particulier dès lors que les modèles se trouvent dans des fichiers différents.

Pour cela, il existe une autre solution :

# fichier Auth.py

class User(Base):
__tablename__ = 'auth_user'
id = Column(Integer, autoincrement=True, primary_key=True)
name = Column(Unicode(16), unique=True, nullable=False)
email_address = Column(Unicode(255), unique=True, nullable=False, info={'rum': {'field':'Email'}})
display_name = Column(Unicode(255))
_password = Column('password', Unicode(128), info={'rum': {'field':'Password'}})
created = Column(DateTime, default=datetime.now)

@classmethod
def __declare_last__(cls):
from infos import Profile
profile = relation(Profile, uselist=False, backref='user')

# fichier infos.py

from auth import User

class Profile(Base):
__tablename__ = 'profile'
id_user = Column(Integer, ForeignKey('authuser.id'), primary_key=True, autoincrement=False)
# Autres champs contenant les données

Il faut bien noter l'emplacement des instructions d'import.

Cela clôt ce mini-tutoriel sur les relations. Il y aurait encore des tonnes de choses à dire, le module SQLAlchemy est clairement un des ORM les plus complets, tout langages confondus, mais ce qui fait sa force, c'est sa simplicité d'utilisation et sa souplesse, car il permet de déclarer ses modèles avec une très grande finesse, une très bonne précision. Et lorsque les modèles sont bien ciselés, le reste n'est plus que formalité !

Merci à ce projet d'exister et merci à ses contributeurs.

Spinner