16

I have a set of tables that look like:

workflows = Table('workflows', Base.metadata,
                  Column('id', Integer, primary_key=True),
                 )

actions = Table('actions', Base.metadata,
                Column('name', String, primary_key=True),
                Column('workflow_id', Integer, ForeignKey(workflows.c.id), primary_key=True),
               )

action_dependencies = Table('action_dependencies', Base.metadata,
                            Column('workflow_id', Integer, ForeignKey(workflows.c.id), primary_key=True),
                            Column('parent_action', String, ForeignKey(actions.c.name), primary_key=True),
                            Column('child_action', String, ForeignKey(actions.c.name), primary_key=True),
                           )

My ORM classes look like:

class Workflow(Base):
    __table__ = workflows

    actions = relationship("Action", order_by="Action.name", backref="workflow")


class Action(Base):
    __table__ = actions

    children = relationship("Action",
                            secondary=action_dependencies,
                            primaryjoin=actions.c.name == action_dependencies.c.parent_action,
                            secondaryjoin=actions.c.name == action_dependencies.c.child_action,
                            backref="parents"
                           )

So in my system, each action is uniquely identified by a combination of a workflow id and its name. I'd like each action to have parents and children attribute that refers its parent and child actions. Each action can have multiple parents and children.

The problem occurs when I have a function such as :

def set_parents(session, workflow_id, action_name, parents):
    action = session.query(db.Action).filter(db.Action.workflow_id == workflow.id).filter(db.Action.name == action_name).one()

    for parent_name in parents:
        parent = session.query(db.Action).filter(db.Action.workflow_id == workflow.id).filter(db.Action.name == parent_name).one()
        action.parents.append(parent)

    session.commit()

I get an error like:

IntegrityError: (IntegrityError) action_dependencies.workflow_id may not be NULL u'INSERT INTO action_dependencies (parent_action, child_action) VALUES (?, ?)' (u'directory_creator', u'packing')

How do I get the relationship to set the workflow_id correctly?

4
  • Why do you need to have workflow_id in the action_dependencies table? Commented May 10, 2012 at 9:06
  • 1
    Because the primary key for an action is a composite of its name and workflow_id. If the workflow_id was not in action_dependencies, there'd be no way to tell which workflow's actions the dependency was referring to. Commented May 10, 2012 at 15:10
  • Good point, good point. let me think... Commented May 10, 2012 at 15:23
  • Please note that your parents/children relationships should also include the workflow_id in the primaryjoin and secondary conditions, or else you will get these for all worklows. Commented May 10, 2012 at 15:33

2 Answers 2

16

See below working code. The key points are those I mentioned in the comments:

  • proper composite ForeignKeys
  • correct relationship configuration using the FKs

Code:

workflows = Table('workflows', Base.metadata,
                  Column('id', Integer, primary_key=True),
                 )

actions = Table('actions', Base.metadata,
                Column('workflow_id', Integer, ForeignKey(workflows.c.id), primary_key=True),
                Column('name', String, primary_key=True),
               )

action_dependencies = Table('action_dependencies', Base.metadata,
                            Column('workflow_id', Integer, ForeignKey(workflows.c.id), primary_key=True),
                            Column('parent_action', String, ForeignKey(actions.c.name), primary_key=True),
                            Column('child_action', String, ForeignKey(actions.c.name), primary_key=True),
                            ForeignKeyConstraint(['workflow_id', 'parent_action'], ['actions.workflow_id', 'actions.name']),
                            ForeignKeyConstraint(['workflow_id', 'child_action'], ['actions.workflow_id', 'actions.name']),
                           )
class Workflow(Base):
    __table__ = workflows
    actions = relationship("Action", order_by="Action.name", backref="workflow")

class Action(Base):
    __table__ = actions
    children = relationship("Action",
                            secondary=action_dependencies,
                            primaryjoin=and_(actions.c.name == action_dependencies.c.parent_action,
                                actions.c.workflow_id == action_dependencies.c.workflow_id),
                            secondaryjoin=and_(actions.c.name == action_dependencies.c.child_action,
                                actions.c.workflow_id == action_dependencies.c.workflow_id),
                            backref="parents"
                           )

# create db schema
Base.metadata.create_all(engine)

# create entities
w_1 = Workflow()
w_2 = Workflow()
a_11 = Action(name="ac-11", workflow=w_1)
a_12 = Action(name="ac-12", workflow=w_1)
a_21 = Action(name="ac-21", workflow=w_2)
a_22 = Action(name="ac-22", workflow=w_2)
session.add(w_1)
session.add(w_2)
a_22.parents.append(a_21)
session.commit()
session.expunge_all()
print '-'*80

# helper functions
def get_workflow(id):
    return session.query(Workflow).get(id)
def get_action(name):
    return session.query(Action).filter_by(name=name).one()

# test another OK
a_11 = get_action("ac-11")
a_12 = get_action("ac-12")
a_11.children.append(a_12)
session.commit()
session.expunge_all()
print '-'*80

# test KO (THIS SHOULD FAIL VIOLATING FK-constraint)
a_11 = get_action("ac-11")
a_22 = get_action("ac-22")
a_11.children.append(a_22)
session.commit()
session.expunge_all()
print '-'*80
Sign up to request clarification or add additional context in comments.

Comments

0

I don't think it's correct to have the primary key a foreign key. How does that work?

But to make composite constraint, a key that is "unique together", use this in the table definition:

UniqueConstraint(u"name", u"workflow_id"),

But if you really want it to be the primary key also you can use this:

PrimaryKeyConstraint(*columns, **kw)

http://docs.sqlalchemy.org/en/latest/core/schema.html#sqlalchemy.schema.PrimaryKeyConstraint

1 Comment

there's nothing wrong with foreign key constraints on primary keys; this is a typical way to get a 'one-to-one' relationship, to map subclasses to the database, or to have attributes that can all be "null together"

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.