On 11/07/2016 05:43 PM, de...@devinfee.com wrote:
When using concrete table inheritance in the complete example below:

 1. why must I return an empty `Column(Integer)` instead of `None`, or
    receive the following error: /"ArgumentError: Mapper

AbstractConcreteBase works in an intricate way in order to get this very unique effect of a class that is mapped, then has it's mapping changed to be something totally different once all the descendant classes have been produced. For this reason, it is more brittle than an ordinary inheriting base class.

In this case, there are four things being done that are not expected by declarative.

The first is that RefClass.__tablename__ returns a non-None value for the RefClass class itself. This has the effect of a "refclass" table being added to the Base.metadata which is not how AbstractConcreteBase works; the "table" to be mapped will ultimately be a big SELECT statement that is a UNION of all the descendant tables. So you don't want a "refclass" table if you're using AbstractConcreteBase.

The second is that the @declared_attr.cascading feature, which is fairly recent, expects that the attribute is being used to map columns in all cases, and it also expects that it is used on a mixin class only. So in this case, it's called only once, it returns None which immediately replaces RefClass.id, and it's gone. "cascading" here should either work on non-mixins or more likely for now report that it's being mis-used if on a non-mixin, https://bitbucket.org/zzzeek/sqlalchemy/issues/3847/declared_attrcascading-does-not-warn-error is added. The reason you are forced to refer to RefClass by name is because the id() def is called before RefClass exists as a symbol in your program.

These two behaviors work together in that one you have __tablename__ return None for RefClass, you no longer need to play those same games with the id() attribute, and the whole thing works.

The third is that @declared_attr overall works completely differently when it returns a non-Column/ mapped value like "None" vs. a column/mapper attribute. The value None can't be used in a mapping so it's assumed that you want to set up RefClass.id = None, that blows away any chance of the attribute being called for ARefClass. The reason this is usually never a problem is because the parent class is either a mixin/__abstract__, in which case it's not called, or a mapped class, in which case it's called and it would return a Column. But here, we're using the the very special / edge-case / brittle AbstractConcreteBase class, which sort of acts like both, and returning None here totally throws it off. So you need to return a column like it has in the example at http://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/api.html?highlight=abstractconcretebase#sqlalchemy.ext.declarative.AbstractConcreteBase. The Column is thrown away for RefClass because RefClass gets mapped to a non-Table object later on. I'm not sure how to make this more intuitive at the moment but I added https://bitbucket.org/zzzeek/sqlalchemy/issues/3848/returning-none-or-anything-from.

The fourth issue is that AbstractConcreteBase is only useful for polymorphic loading, and for it to be worth the effort here you need concrete=True and polymorphic discriminators on your classes. If you aren't going for polymorphic loading using a giant UNION statement (see example below), don't use AbstractConcreteBase, just use a normal base with __abstract__ = True.


class RefClass(AbstractConcreteBase, Base):

    @declared_attr
    def __tablename__(cls):
        if cls.__name__ == 'RefClass':
            return None
        return cls.__name__.lower()

    @declared_attr
    def id(cls):
        if cls.__name__ == 'RefClass':
            return Column(Integer)
        column_name = '{}.id'.format(cls.ref.__tablename__)
        return Column(ForeignKey(column_name), primary_key=True)


class ARefClass(RefClass):
    ref = AClass

    __mapper_args__ = {"concrete": True, "polymorphic_identity": 'a'}


class BRefClass(RefClass):
    ref = BClass

    __mapper_args__ = {"concrete": True, "polymorphic_identity": 'b'}


output of db.query(RefClass).all():

 SELECT pjoin.id AS pjoin_id, pjoin.type AS pjoin_type
FROM (SELECT brefclass.id AS id, 'b' AS type
FROM brefclass UNION ALL SELECT arefclass.id AS id, 'a' AS type
FROM arefclass) AS pjoin








    Mapper|ARefClass|arefclass could not assemble any primary key
    columns for mapped table 'arefclass'"/
 2. why must I reference the class by name and not type in
    `@declarad_attr`, unlike in a `classmethod`?
 3. how does the `__tablename__` `declaredattr` actually behave any
    differently than the `id` `declaradattr`?








from sqlalchemy import (
    Column,
    ForeignKey,
    Integer,
    create_engine,
)
from sqlalchemy.ext.declarative import (
    AbstractConcreteBase,
    declared_attr,
    declarative_base,
    has_inherited_table,
)
from sqlalchemy.orm import Session

Base = declarative_base()


class AClass(Base):
    __tablename__ = 'aclass'
    id = Column(Integer, primary_key=True)


class BClass(Base):
    __tablename__ = 'bclass'
    id = Column(Integer, primary_key=True)


class RefClass(AbstractConcreteBase, Base):
    @declared_attr
    def __tablename__(cls):
        return cls.__name__.lower()

    @declared_attr.cascading
    def id(cls):
        if cls.__name__ != 'RefClass':
            column_name = '{}.id'.format(cls.ref.__tablename__)
            return Column(ForeignKey(column_name), primary_key=True)
        else:
            # return  # Fails as described in Q1
            return Column(Integer)


class ARefClass(RefClass):
    ref = AClass


class BRefClass(RefClass):
    ref = BClass


engine = create_engine('sqlite://', echo=True)
Base.metadata.bind = engine
Base.metadata.create_all()
db = Session(engine)


Thanks!
Devin

--
SQLAlchemy -
The Python SQL Toolkit and Object Relational Mapper

http://www.sqlalchemy.org/

To post example code, please provide an MCVE: Minimal, Complete, and
Verifiable Example. See http://stackoverflow.com/help/mcve for a full
description.
---
You received this message because you are subscribed to the Google
Groups "sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send
an email to sqlalchemy+unsubscr...@googlegroups.com
<mailto:sqlalchemy+unsubscr...@googlegroups.com>.
To post to this group, send email to sqlalchemy@googlegroups.com
<mailto:sqlalchemy@googlegroups.com>.
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

--
SQLAlchemy - The Python SQL Toolkit and Object Relational Mapper

http://www.sqlalchemy.org/

To post example code, please provide an MCVE: Minimal, Complete, and Verifiable 
Example.  See  http://stackoverflow.com/help/mcve for a full description.
--- You received this message because you are subscribed to the Google Groups "sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to sqlalchemy+unsubscr...@googlegroups.com.
To post to this group, send email to sqlalchemy@googlegroups.com.
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to