ORM relationship backref missing unless manually call configure_mappers()

Issue #3389 wontfix
Marcus Cobden created an issue

This is with SQLAlchemy 1.0.2

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


logging.basicConfig(level=logging.DEBUG)
Base = declarative_base()


class Project(Base):
    __tablename__ = 'projects'
    # Internal database ID
    _id = Column(Integer, primary_key=True)
    # API id
    slug = Column(String, unique=True)
    runs = relationship(
        'Run',
        backref='project',
        cascade="all, delete-orphan")

class Run(Base):
    __tablename__ = 'runs'
    _id = Column(Integer, primary_key=True)
    _project_id = Column(Integer, ForeignKey('projects._id'), nullable=False)

    number = Column(Integer, unique=True, autoincrement=True)


# configure_mappers()


print Project.runs # works 'Project.runs'
print Run.project # Attribute Error!

Comments (5)

  1. Mike Bayer repo owner

    this isn't a 1.0 regression. 0.9, 0.8, 0.7 etc will all do the same thing. We don't automatically call configure_mappers() until the mapping is used to produce instances, either querying or persisting. Otherwise, mappings that may wish to use Project.runs in order to construct additional SQL expressions to be mapped will attempt to configure the mapper and then fail to find dependencies that aren't imported yet. Essentially, we can fix this to work either way, and we will fail on one side or the other.

    this is why the current documentation starts off with the better (but more verbose) way to do this, which is to use back populates, so that each class has the relationship set up up front:

    class Project(Base):
        __tablename__ = 'projects'
        # Internal database ID
        _id = Column(Integer, primary_key=True)
        # API id
        slug = Column(String, unique=True)
        runs = relationship(
            'Run',
            back_populates="project",
            cascade="all, delete-orphan")
    
    
    class Run(Base):
        __tablename__ = 'runs'
        _id = Column(Integer, primary_key=True)
        _project_id = Column(Integer, ForeignKey('projects._id'), nullable=False)
    
        number = Column(Integer, unique=True, autoincrement=True)
    
        project = relationship("Project", back_populates="runs")
    
  2. Marcus Cobden reporter

    Ah, thanks, that makes sense. It might also be a good idea for the docs on backref to mention that declaration is deferred until later, if they don't already.

    Another option would be to assign a placeholder attribute until the mapping is configured, but I'm not sure that would gain anything, and might cause unexpected behavior if someone caches a reference to it.

  3. Mike Bayer repo owner

    OK so, technically, when you do relationship(), the thing that's there is already somewhat of a "placeholder" in that it is the InstrumentedAttribute, but it doesn't have the "impl" set up yet, which is the guts of it that handles working with instances.

    The "backref" keyword in relationship() could work this way as well, in that when relationship() sees it, it does part of its work immediately so that the other attribute is built up on the other class. But the problem is exactly the one that is why we have configure_mappers() in the first place; it requires locating the other class and doing something to it immediately, and if that other class doesn't exist yet (which will be about 50% of all mappings), it's not possible. We'd need some kind of event system so that when a class named "X" gets created, some initial things run on it, which means we're building a three-stage mapper config and things are getting totally out of hand at that point.

    that's why back_populates is so much better. We move out the configuration back to the way classes are laid out and no complicated guesswork and multi-stage event-based steps need to be built. It is also conceptually simpler to the end-user.

  4. Log in to comment