On 12/12/2015 09:56 PM, Gerald Thibault wrote:
> I am loading data from json files, and then creating instances via
> ObjClass(**data), loading them into a session, and committing. I do not
> see an easy way to go from having an integer foreign_key value to
> populating the relationship with the corresponding model in order to
> have post_update work. 
> 
> As an example, a fixture file might look like this:
> 
> [
> {
>     "__model__": "Widget",
>     "widget_id": 1,
>     "favorite_entry_id": 1,
>     "name": "somename"
> },
> {
>     "__model__": "Entry",
>     "entry_id": 1,
>     "widget_id": 1,
>     "name": "somename"
> }
> ]
> 
> The fixture loader deserialiizes the file contents, looks up the
> __model__ in the SQLA class registry, creates the instance via
> Model(**data), adds to the session, and once all instances have been
> added, it commits. Do you know a way I could have post_update take
> effect given the format of the data that I am starting with? 

You're already creating Model() objects in response to reading from a
JSON document, so the work of "translate from integer identifiers to
objects" is already what's going on.  So extending this to some of the
related attributes in the JSON format should be straightforward.




> Would I
> need to introspect the relationships, determine which ones are
> post_update, get the remote class of the rel via introspection, 


I'm not sure why this system needs to work based on introspection.
that's certainly possible, but I'm looking to challenge the narrative of
this is "hard" / "complicated" by default (not like the introspection
path is very difficult either but it is a little less trivial than not
going that path).

Your JSON already contains the information it needs, e.g. "__model__",
which is the class.  This is OOP, a "class" encapsulates data and
methods which work on that data, and the relationship() attributes on
these mapped classes is in fact part of that encapsulation.  The most
simple way to build this out is just to add additional hints that are
consumable.   If you wanted to dynamically construct these hints w/
introspection, that's doable, but someone is already typing out
"relationship(...)", just type out some hints for deserialize as well.
Example follows which includes some of the "introspection" version as
well as the "static" version, maybe this can give you some ideas.

from sqlalchemy import Column, Integer, ForeignKey, create_engine, String
from sqlalchemy.orm import relationship, Session
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import inspect
from sqlalchemy.orm.interfaces import MANYTOONE

Base = declarative_base()


class Entry(Base):
    __tablename__ = 'entry'
    entry_id = Column(Integer, primary_key=True)
    widget_id = Column(Integer, ForeignKey('widget.widget_id'))
    name = Column(String(50))

    # static version
    _deferred_attrs = {
        'widget_id': ('widget', "Widget")
    }


class Widget(Base):
    __tablename__ = 'widget'

    widget_id = Column(Integer, primary_key=True)
    favorite_entry_id = Column(
        Integer,
        ForeignKey('entry.entry_id', name="fk_favorite_entry"))
    name = Column(String(50))

    entries = relationship(
        Entry, primaryjoin=widget_id == Entry.widget_id, backref='widget')

    favorite_entry = relationship(
        Entry,
        primaryjoin=favorite_entry_id == Entry.entry_id,
        post_update=True)

    # static version
    # _deferred_attrs = {
    #    'favorite_entry_id': ('favorite_entry', "Entry")
    # }

    # introspection version
    @property
    def _deferred_attrs(self):
        mapper = inspect(self).mapper
        return dict(
            (
                # the big assumption here is that we're dealing with
                # only single-column primary / foreign keys.  accommodating
                # more complicated kinds of relationships would require more
                # logic here to either skip them or handle them,
depending on
                # your use case
                list(rel.local_remote_pairs)[0][0].key,
                (rel.key, rel.mapper.class_.__name__)
            )
            for rel in mapper.relationships if rel.direction is MANYTOONE
        )


model_registry = {'Widget': Widget, 'Entry': Entry}

e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

data = [
    {
        "__model__": "Widget",
        "widget_id": 1,
        "favorite_entry_id": 1,
        "name": "somename"
    },
    {
        "__model__": "Entry",
        "entry_id": 1,
        "widget_id": 1,
        "name": "somename"
    }
]

# putting everything into a dictionary first.  If your database model
# allows columns to be NULLABLE up front, you can instead use
# session.merge() to put rows straight into the database, it's just you
# might have more UPDATE statements in the end.
objs = {}
for rec in data:
    cls = model_registry[rec['__model__']]

    our_rec = dict(
        (k, v) for k, v in rec.items() if not k.startswith("__")
    )

    obj = cls(**our_rec)

    # this is a mini-identity map, since we aren't in the Session
    # yet.  A little bit of introspection here, but classes could
    # also include a "get_my_primary_key()" method just as well.
    objs[
        (cls.__name__,
         # assuming non-composite primary keys.
         inspect(obj).mapper.primary_key_from_instance(obj)[0])
    ] = obj

# reconcile deferred attributes
for obj in objs.values():
    for fk_attr, (target_attr, target_cls) in obj._deferred_attrs.items():
        target_obj = objs[(target_cls, getattr(obj, fk_attr))]
        delattr(obj, fk_attr)
        setattr(obj, target_attr, target_obj)

session = Session(e)
session.add_all(objs.values())
session.commit()





-- 
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