SAWaring when using a Mixin to make SQLAlchemy Objects

33 views Asked by At

I have a mixin that is shared between two models. In it, I have a method that takes a dictionary and creates an SQLAlchemy object, this is raising a warning. The two models are very similar (all the same except for one column that has a relationship to a different table. However, all the column names have the same names.

Here is the mixin:

class ConstructionBase:
    id: Mapped[int] = mapped_column(db.Integer, autoincrement=True, primary_key=True)   
    data: Mapped[bytes] = mapped_column(db.LargeBinary, unique=False, nullable=True)
    measurement_date: Mapped[datetime] = mapped_column(db.DateTime, nullable=True)
    
    def from_dict(self, data):
        if 'module' in data:
            self.module = Module.get_by_sn(data['module'])
        if 'component' in data:
            self.component = Component.get_by_sn(data['component'])
        if 'type' in data:
            TypeClass = type(self).type.property.mapper.class_
            self.type = TypeClass.get_by_name(data['type'])  

And my two models:

class Assembly(db.Model, UtilityMixin, PaginatedAPIMixin, TimeStampMixin, MTDdbSyncMixin, ConstructionBase):
    __tablename__ = "assembly"
    module_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('module.id', ondelete="CASCADE"), nullable=True)
    component_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('component.id', ondelete="CASCADE"), nullable=True)
    type_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('assembly_type.id'), nullable=False)

    #Relationships
    module: Mapped["Module"] = relationship(back_populates="assembly")
    component: Mapped[List["Component"]] = relationship(back_populates="assembly")
    type: Mapped["AssemblyType"] = relationship(back_populates="assembly")

class Test(db.Model, UtilityMixin, PaginatedAPIMixin, TimeStampMixin, MTDdbSyncMixin, ConstructionBase):
    __tablename__ = "test"
    module_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('module.id', ondelete="CASCADE"), nullable=True)
    component_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('component.id', ondelete="CASCADE"), nullable=True)
    type_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('test_type.id'), nullable=False)

    #Relationships
    module: Mapped["Module"] = relationship(back_populates="test") 
    component: Mapped["Component"] = relationship(back_populates="test")
    type: Mapped["TestType"] = relationship(back_populates="test")

With this mixin I would can do something like

fake_assembly = Assembly()
fake_assembly.from_dict({'module':'mod_sn','component':'comp_sn','type':'the type'})

However when I do that I get this warning:

/home/application/models.py:656: SAWarning: Object of type <Assembly> not in session, add operation along 'Module.assembly' will not proceed (This warning originated from the Session 'autoflush' process, which was invoked automatically in response to a user-initiated operation.)
  return Component.query.filter_by(serial_number=serial_number).first()

But the object works, when I print it out it is <Assembly None> which is wrong but when I do fake_assembly.module it gives the correct module object. And I can add it to the session and commit it just fine. Also, it only throws the error for objects that have relationships to other tables.

In regards to the warning I have been unable to find anything useful, so I have come here to try to learn about it. Maybe the way I am currently trying to do things is bad practice. Any insight or help is greatly appreciated!

1

There are 1 answers

2
Ian Wilson On BEST ANSWER

I think somehow you are flushing the assembly when the second lookup occurs: Component.get_by_sn(). So you connect a module, and then that module includes the assembly via back_populates. Then the second get_by_sn triggers the flush of the module and the session doesn't know what to do with the assembly connected to the module because you haven't finished building it yet and it isn't in the session.

I guess there are a lot of practices. I would probably consider a better practice to move the factory pattern outside the class entirely or second best at least put it on a classmethod.

Classmethod factory

This would be added via the mixin.

class ConstructionBase:
    @classmethod
    def from_dict(cls, data):
        kwargs = {}
        if 'module' in data:
            kwargs['module'] = Module.get_by_sn(data['module'])
        if 'component' in data:
            kwargs['component'] = Component.get_by_sn(data['component'])
        if 'type' in data:
            TypeClass = cls.type.property.mapper.class_
            kwargs['type'] = TypeClass.get_by_name(data['type']) 
        return cls(**kwargs)

# Called like
assembly = Assembly.from_dict(data)

Standalone factory

def construct_from_dict(cls, data):
    kwargs = {}
    if 'module' in data:
        kwargs['module'] = Module.get_by_sn(data['module'])
    if 'component' in data:
        kwargs['component'] = Component.get_by_sn(data['component'])
    if 'type' in data:
        TypeClass = cls.type.property.mapper.class_
        kwargs['type'] = TypeClass.get_by_name(data['type']) 
    return cls(**kwargs)

# Called like
assembly = construct_from_dict(Assembly, data)