简体   繁体   中英

Association tables with multiple foreign-key relationships on one column in SQLAlchemy + SQLite

tldr; In the SQLAlchemy ORM Mapper class docs , an example is provided for how to create association tables many-to-many relationship between two tables, like so:

一父一子关联表

But how can I create a many-to-many association with four parent tables and a single child table? In the E/R diagram below, keyword , solution , pattern , and question are the parent tables, data_lineage_lookup is the association table, and data_lineage is the child table.

四个父母和一个孩子的关联表


Background info: I have three tables in my database to organize data lineage metadata. The first table, data_lineage_lookup , is an association table (many-to-many) where the id column has foreign-key relationships with four other tables: question , solution , pattern , and keyword . In data_lineage_lookup , each id in the id column is unique. Each such id is paired with a data_lineage_id .

The data_lineage_id is a foreign key to the data_lineage table, which holds metadata about where the data was sourced from in the form of json documents kept in the json_body column.

Additionally, each json document in the data_lineage table has a corresponding json_schema (many-to-one relationship where many documents share the same json schema )

-- association table I would like to recreate
CREATE TABLE IF NOT EXISTS data_lineage_lookup (
    id TEXT NOT NULL, 
    data_lineage_id TEXT NOT NULL, 
        -- text id's of any kind can be used to look up data lineage
        FOREIGN KEY (id) REFERENCES question (id) ON DELETE CASCADE,
        FOREIGN KEY (id) REFERENCES solution (id) ON DELETE CASCADE,
        FOREIGN KEY (id) REFERENCES pattern  (id) ON DELETE CASCADE,
        FOREIGN KEY (id) REFERENCES keyword  (id) ON DELETE CASCADE,
        FOREIGN KEY (data_lineage_id) REFERENCES data_lineage  (id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS data_lineage (
    id TEXT NOT NULL PRIMARY KEY DEFAULT (lower(hex(randomblob(4)))),
    json_schema_id INTEGER NOT NULL,
    json_body         TEXT NOT NULL,
    date_found_timestamp INTEGER NOT NULL,
        FOREIGN KEY (json_schema_id) REFERENCES json_schema (id) ON DELETE RESTRICT
);

CREATE TABLE IF NOT EXISTS json_schema (
    id TEXT NOT NULL PRIMARY KEY DEFAULT (lower(hex(randomblob(4)))),
    title                   TEXT NOT NULL,
    body                    TEXT NOT NULL,
    date_added_timestamp INTEGER NOT NULL
);

The question: According to the documentation on many-to-many relationships in ORM mapper classes , the following example serves to illustrate how to use an association table and relationship() columns to establish a many-to-many relationship, but here the association table has two columns – one for each table it associates with, and so the use of relationship() is straightforward.

association_table = Table('association', Base.metadata,
    Column('left_id', ForeignKey('left.id')),
    Column('right_id', ForeignKey('right.id'))
)

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship("Child",
                    secondary=association_table)

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)

I've already declared the mapper classes below in SQLAlchemy, but How can I configure the data_lineage_lookup.id column so it serves as a single foreign-key column that map to 4 different tables: question , solution , pattern , and keyword through a relationship() in the JsonSchema ORM Mapper Class?

'''
Document Metadata Schema
'''

@declarative_mixin
class DocumentMetadata:

    id = Column(Text, nullable=False, primary_key=True, default=text('(lower(hex(randomblob(4))))'), server_default=text('(lower(hex(randomblob(4))))'))
    body = Column(JSON, nullable=False) # TODO: check datatype after loading data with `select typeof(col) from table;`
    date_added = Column(DATETIME, nullable=False)

    def __repr__(self):
        return f"<{self.__class__.__name__}{self.__dict__}>"

    @declared_attr
    def __tablename__(cls):
        return re.sub(r'(?<!^)(?=[A-Z])', '_', cls.__name__).lower()


data_lineage_lookup = Table(
    'data_lineage_lookup',
    Base.metadata,
    Column('id',
        ForeignKey('question.id', ondelete="CASCADE"),
        ForeignKey('solution.id', ondelete="CASCADE"),
        ForeignKey('pattern.id', ondelete="CASCADE"),
        ForeignKey('keyword.id', ondelete="CASCADE")
    ),
    Column('data_lineage_id', ForeignKey('data_lineage.id', ondelete="CASCADE"))
)

class JsonSchema(Base, DocumentMetadata):
    title = Column(Text, nullable=False)

class DataLineage(Base, DocumentMetadata):
    json_schema_id = Column(Text, ForeignKey('json_schema.id', ondelete='RESTRICT'), nullable=False)

The solution was to simply treat the keyword , question , pattern , solution ORM mapper classes as parent tables, and the data_lineage ORM mapper like child tables, all of which share the same association table, data_lineage_lookup .

So the code for the association table looks like this:

data_lineage_lookup = Table(
    'data_lineage_lookup',
    Base.metadata,
    Column(
        'id',
        ForeignKey('question.id', ondelete="CASCADE"),
        ForeignKey('solution.id', ondelete="CASCADE"),
        ForeignKey('pattern.id', ondelete="CASCADE"),
        ForeignKey('keyword.id', ondelete="CASCADE"),
        nullable=False
    ),
    Column('data_lineage_id', ForeignKey('data_lineage.id', ondelete="CASCADE"), nullable=False)
)

and each data ORM mapper class should have a data_lineages attribute as follows

class Keyword(Base, Document):
    patterns = relationship(
        'Pattern',
        secondary=keyword_lookup,
        back_populates='keywords'
    )
    data_lineages = relationship( #<- like this
        'DataLineage', 
        secondary=data_lineage_lookup
    )

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM