Adam Donahue
Adam Donahue

Reputation: 1658

Dynamic relationship class loading with sqlalchemy

I have a library via which I dynamically load classes. It's exposed as

mylib.registry.<className>

registry is an instance of a class that contains a dictionary of class name (strings) to module names, and a getattr call that dynamically loads a class if it is requested. A user can thus refer to any class without having to deal with the module locations (there is a global namespace for class names, but not module names).

For example, the entries:

{'X', 'mylib.sublib.x',
 'Y', 'mylib.sublib.y'}

could then be used like:

import mylib

x = mylib.registry.X()
y = mylib.registry.Y()

That's the background. On top of this, these objects are sqlalchemy ORM classes which have relationships to one another. Let's assume here that X has a one-to-many with Y.

Assume thus this definition.

class X(Base):
    y_id = Column(Integer, ForeignKey('y.id'))
    y = relationship('Y')

class Y(Base):
    xs = relationship('X')

These are in separate files, and each imports the top-level registry.

So here's the issue -- how do I actually get this to resolve without loading every class up front?

The example above doesn't work, because if I import only X via the registry, then Y isn't in sqlalchemy class registry, and thus the relatiobship breaks.

If I import the registry itself and then refer to the classes directly, then the modules don't load because of interdependencies.

I tried using a lambda to defer loading, but this too fails with an error about a missing 'strategy'.

What approaches have others used here? If I'm missing something obvious, let me know. It's been a long day.

Thanks.

Upvotes: 2

Views: 1422

Answers (1)

javex
javex

Reputation: 7544

You should never use relationships on two sides that don't know about each other. Normally, this is avoided by using backref but in your case this creates problems, because you want each side to be aware of its relationship by itself. The trick here is the back_populates keyword, offered by relationship:

y = relationship("Y", back_populates="xs")

and

xs = relationship("X", back_populates="y")

Applying these will make them aware of each other. However, it will not solve your importing problem. Normally, you could now just from x import X on the Y side. But the other way around won't work because it will create a circular import.

The trick is simple: Put the import after the class you want to import. Because the strings in relationship are evaluated lazily, you can import the class after the relationship was defined. So for X do this:

class X(Base):
    __tablename__ = 'x'
    id = Column(Integer, primary_key=True)
    y_id = Column(ForeignKey('y.id'))
    y = relationship("Y", back_populates="xs")

from y import Y

And the other way around for the Y as well (this is not required, but creates more symmetry). I'd also put a comment there to explain this to avoid someone putting it to the top, breaking the program.

Upvotes: 1

Related Questions