Using relationship with primaryjoin in declared_attr fails

Issue #2876 resolved
Wichert Akkerman created an issue

(Almost) minimal test case:

import sqlalchemy
from sqlalchemy import schema
from sqlalchemy import types
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.declarative import declared_attr


metadata = schema.MetaData()
BaseObject = declarative_base(metadata=metadata)


class Image(BaseObject):
    __tablename__ = 'image'
    id = schema.Column(types.Integer(), primary_key=True, autoincrement=True)


class AvatarMixin(object):
    @declared_attr
    def avatar_original_id(cls):
        return schema.Column('avatar_original_id', types.Integer(),
                schema.ForeignKey('image.id', onupdate='CASCADE', ondelete='RESTRICT'),
                unique=True)

    @declared_attr
    def avatar_original(cls):
        return relationship(Image, cascade='all',
                primaryjoin=(cls.avatar_original_id == Image.id))

    @declared_attr
    def avatar_id(cls):
        return schema.Column('avatar_id', types.Integer(),
                schema.ForeignKey('image.id', onupdate='CASCADE', ondelete='RESTRICT'),
                unique=True)


class User(BaseObject, AvatarMixin):
    __tablename__ = 'user'
    id = schema.Column(types.Integer(), primary_key=True, autoincrement=True)

engine = sqlalchemy.create_engine('sqlite:///:memory:')
metadata.create_all(engine)
User()

results in this output:

...
  File "/Users/wichert/Library/buildout/eggs/SQLAlchemy-0.8.3-py2.7-macosx-10.9-x86_64.egg/sqlalchemy/schema.py", line 1026, in references
    if fk.column.proxy_set.intersection(column.proxy_set):
  File "/Users/wichert/Library/buildout/eggs/SQLAlchemy-0.8.3-py2.7-macosx-10.9-x86_64.egg/sqlalchemy/util/langhelpers.py", line 612, in __get__
    obj.__dict__[self.__name__](self.__name__) = result = self.fget(obj)
  File "/Users/wichert/Library/buildout/eggs/SQLAlchemy-0.8.3-py2.7-macosx-10.9-x86_64.egg/sqlalchemy/schema.py", line 1456, in column
    if schema is None and parenttable.metadata.schema is not None:
AttributeError: 'NoneType' object has no attribute 'metadata'

Comments (2)

  1. Mike Bayer repo owner

    TL;DR; - this situation is documented at http://docs.sqlalchemy.org/en/rel_0_9/orm/extensions/declarative.html#mixing-in-relationships

    relationship() definitions which require explicit primaryjoin, order_by etc. expressions should use the string forms for these arguments, so that they are evaluated as late as possible. To reference the mixin class in these expressions, use the given cls to get its name:
    

    The error message here is improved in 0.9, where you will get:

      File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/sql/schema.py", line 1485, in _resolve_col_tokens
        "this ForeignKey's parent column is not yet associated "
    sqlalchemy.exc.InvalidRequestError: this ForeignKey's parent column is not yet associated with a Table.
    

    Your method avatar_original_id creates a new Column object each time it is called. When you call it within your avatar_original method, you get a Column back, but that Column object is unknown to any other aspect of the program.

    So the class gets mapped normally, declarative calls upon avatar_original_id to get at the Column that will actually be mapped, and the one you've put in your relationship() has no parent table.

    The solution - except in the most simplistic cases, strings or lambdas should always be used for relationship arguments in declarative:

        @declared_attr
        def avatar_original(cls):
            return relationship(Image, cascade='all',
                    primaryjoin=lambda: (cls.avatar_original_id == Image.id))
    

    or

        @declared_attr
        def avatar_original(cls):
            return relationship(Image, cascade='all',
                    primaryjoin="%s.avatar_original_id == Image.id" % cls.__name__)
    

    the latter is documented in the last example at: http://docs.sqlalchemy.org/en/rel_0_9/orm/extensions/declarative.html#mixing-in-relationships

    the "lambda:" form should probably be added to the docs as it is handy.

  2. Log in to comment