具有三个相互关联的表的多对多(SqlAlchemy)

时间:2018-10-21 23:13:27

标签: python sqlalchemy many-to-many flask-sqlalchemy

我有三个表User,Device和Role。我已经创建了这样的多对多的黑白用户和设备关系;

#Many-to-Many relation between User and Devices
userDevices = db.Table("user_devices",
                       db.Column("id", db.Integer, primary_key=True),
                       db.Column("user_id", db.Integer, db.ForeignKey("user.id")),
                       db.Column("device_id", db.Integer, db.ForeignKey("device.id"))))

class User(db.Model):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(60), index=True, unique=True)
    devices = db.relationship("Device", secondary=userDevices, backref=db.backref('users'), lazy="dynamic")

class Device(db.Model):
    __tablename__ = 'device'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(60), unique=True)

这很好运作。我可以将设备d1分配给用户u1> d1.users.append(u1),并将用户分配给设备> u1.devices.append(d1)db.session.commit()

我想要的更多是扩展表user_devices并在其中添加一列作为role_id,这将成为Role表的ForeignKey。因此,此表user_devices将清楚地描述特定Role上特定User的{​​{1}}。在表Device中添加列role_id后,我将user_devices表描述为;

Role

以这种方式,如何为设备class Role(db.Model): __tablename__ = 'role' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(60), unique=True) device = db.relationship("Device", secondary=userDevices, backref=db.backref('roles'), lazy="dynamic") 上的用户r1分配角色u1? 这是我尝试过的:

d1

这将在表# First get the device, user and role deviceRow = db.session.query(Device).filter(Device.name=="d1").first() userRow = db.session.query(User).filter(User.username=="u1").first() roleRow = db.session.query(Role).filter(Role.name == "r1").first() # Then add the user on that device deviceRow.users.append(userRow) deviceRow.roles.append(roleRow)

中创建两行

enter image description here

有没有办法像这样将两个属性添加到表中?;

user_devices

以便仅在commit()之后创建一行?

2 个答案:

答案 0 :(得分:2)

3个实体的关联不再是简单的多对多关系。您需要的是association object模式。为了使关联的处理更加容易,将其映射为模型类而不是简单的Table

class UserDevice(db.Model):
    __tablename__ = "user_devices"

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
    device_id = db.Column(db.Integer, db.ForeignKey("device.id"), nullable=False)
    role_id = db.Column(db.Integer, db.ForeignKey("role.id"), nullable=False)

    __table_args__ = (db.UniqueConstraint(user_id, device_id, role_id),)

    user = db.relationship("User", back_populates="user_devices")
    device = db.relationship("Device")
    role = db.relationship("Role", back_populates="user_devices")

class User(db.Model):
    __tablename__ = "user"
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(60), index=True, unique=True)
    user_devices = db.relationship("UserDevice", back_populates="user")

class Role(db.Model):
    __tablename__ = "role"

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(60), unique=True)
    user_devices = db.relationship("UserDevice", back_populates="role")

要将用户与设备和角色相关联,请创建一个新的UserDevice对象:

device = db.session.query(Device).filter(Device.name == "d1").first()
user = db.session.query(User).filter(User.username == "u1").first()
role = db.session.query(Role).filter(Role.name == "r1").first()
assoc = UserDevice(user=user, device=device, role=role)
db.session.add(assoc)
db.session.commit()

请注意,ORM关系不再是Device等对象的简单集合,而是UserDevice对象。这是一件好事:例如,当您遍历user.user_devices时,您会获得有关设备以及用户在设备上所扮演的角色的信息。如果确实希望在不需要角色信息的情况下也提供更简单的集合,则可以使用associationproxy

答案 1 :(得分:1)

有一种方法可以使三对多关系不是两个多对多关系的组合。您需要an association object,因为仅使用表的语法不允许3对多对多(因为secondary明确指2对多对多)。

以下是一般情况下的最小示例:

from sqlalchemy import ForeignKey, Column, Integer, String
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base, declared_attr

Base = declarative_base()

# Helper classes to simplify the other classes:
#  1. Adds ch column
#  2. Defines how to print it
class Ch:
  ch = Column(String, nullable=False)
  def __str__(self):
    return self.ch

#  3. Automatically determines table name (for foreign key)
class AutoNamed:
  @declared_attr
  def __tablename__(cls):
    return cls.__name__

class ABC(AutoNamed, Base):
  a_id = Column(Integer, ForeignKey('A.a_id'), primary_key=True)
  b_id = Column(Integer, ForeignKey('B.b_id'), primary_key=True)
  c_id = Column(Integer, ForeignKey('C.c_id'), primary_key=True)
  a = relationship('A', back_populates='abcs')
  b = relationship('B', back_populates='abcs')
  c = relationship('C', back_populates='abcs')
  def __repr__(self):
    return f'{self.a} {self.b} {self.c}'

class A(Ch, AutoNamed, Base):
  a_id = Column(Integer, primary_key=True)
  abcs = relationship('ABC', back_populates='a')

class B(Ch, AutoNamed, Base):
  b_id = Column(Integer, primary_key=True)
  abcs = relationship('ABC', back_populates='b')

class C(Ch, AutoNamed, Base):
  c_id = Column(Integer, primary_key=True)
  abcs = relationship('ABC', back_populates='c')

好的,现在来一点解释:

  • ABC是一个关联表,需要3对多对多表中每个表的单个实例。
  • ABC中的每一个都将引用所有ABC对象的引用,这些对象涉及在实例化ABC实例时自动添加的涉及它们的对象。 / li>
  • 有一个陷阱:当您使用relationship.secondary时,对象上的属性是其他类型的列表(在这种情况下,parent.childrenChildren对象的列表) 。但是,在“关联对象”的文档中,将其翻译为关联对象时,尽管它们仍在parent对象children上命名属性,但实际上它是Association对象的列表。在这里,我通过调用属性abcs来明确这一点。

您可以像平常一样实例化这些:

anA = A(ch='x')
anB = B(ch='y')
anC = C(ch='z')
anABC = ABC(a=anA, b=anB, c=anC)

sess.add(anABC)

作为健全性检查,以下是从中为SQLite生成的SQL。正是我们所期望的。

CREATE TABLE "A" (
  ch VARCHAR NOT NULL, 
  a_id INTEGER NOT NULL, 
  PRIMARY KEY (a_id)
);

CREATE TABLE "B" (
  ch VARCHAR NOT NULL, 
  b_id INTEGER NOT NULL, 
  PRIMARY KEY (b_id)
);

CREATE TABLE "C" (
  ch VARCHAR NOT NULL, 
  c_id INTEGER NOT NULL, 
  PRIMARY KEY (c_id)
);

CREATE TABLE "ABC" (
  a_id INTEGER NOT NULL, 
  b_id INTEGER NOT NULL, 
  c_id INTEGER NOT NULL, 
  PRIMARY KEY (a_id, b_id, c_id), 
  FOREIGN KEY(a_id) REFERENCES "A" (a_id), 
  FOREIGN KEY(b_id) REFERENCES "B" (b_id), 
  FOREIGN KEY(c_id) REFERENCES "C" (c_id)
);