Update a (custom) dictionary in a dict-of-dict model #6321
-
Dear all, I implemented a dictionary of dictionaries model. My main entity However, I want to use a custom dictionary as inner dict that is derived from Best, Nicolas from sortedcontainers import SortedDict
import datetime as dt
# sqlalchemy version: 1.3.18
from sqlalchemy import create_engine
from sqlalchemy import Column, Integer, Float, String, Date
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Session, relationship
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
Base = declarative_base()
class MyDict(SortedDict):
"""custom sorted dictionary stuff"""
pass
def mydict_getset_factory(*args, **kwargs):
def getter(obj):
print("getter called")
return MyDict(getattr(obj, "series")) # cast dict to MyDict
def setter(obj, key, value):
print("setter called")
return setattr(obj, "series", value)
return getter, setter
def mydict_creator(key, value):
print("creator called")
return Item(item_date=key, series=value)
class Curve(Base):
__tablename__ = "curve"
name = Column(String, primary_key=True)
_items = relationship(
"Item",
collection_class=attribute_mapped_collection("item_date"),
)
items = association_proxy(
target_collection="_items",
attr="series",
creator=mydict_creator,
getset_factory=mydict_getset_factory,
)
class Item(Base):
__tablename__ = "item"
id = Column(Integer, primary_key=True)
curve_id = Column(Integer, ForeignKey("curve.name"), nullable=False)
item_date = Column(Date, nullable=False)
_series = relationship(
"KeyValuePair",
collection_class=attribute_mapped_collection("key"),
cascade="all, delete, delete-orphan",
)
series = association_proxy(
target_collection="_series",
attr="value",
creator=lambda k, v: KeyValuePair(key=k, value=v),
)
def __init__(self, item_date, series=None):
self.item_date = item_date
if series:
self.series = series
class KeyValuePair(Base):
__tablename__ = "key_value_pair"
id = Column(Integer, primary_key=True)
item_id = Column(Integer, ForeignKey("item.id"), nullable=False)
key = Column(Float)
value = Column(String)
def __init__(self, key, value):
self.key = key
self.value = value
if __name__ == "__main__":
engine = create_engine("sqlite://", echo=False)
Base.metadata.create_all(engine)
session = Session(engine)
#
# update on MyDict directly
#
a = MyDict({0.25: "A", 0.5: "B"})
b = MyDict({0.5: "C", 1.0: "D"})
print(a) # MyDict({0.25: "A", 0.5: "B"})
print(type(a))
a.update(b)
print(a) # MyDict({0.25: "A", 0.5: "C", 1.0: "D"})
print("=====")
#
# update on MyDict using SQLAlchemy
#
a = MyDict({0.25: "A", 0.5: "B"})
b = MyDict({0.5: "C", 1.0: "D"})
d = dt.date(2021, 12, 31)
curve = session.query(Curve).filter_by(name="curve").one_or_none()
if curve is None:
curve = Curve(name="curve")
curve.items[d] = a # calls creator
curve.items[d] = a # calls setter
tmp = curve.items[d] # calls getter
print(tmp) # MyDict({0.25: 'A', 0.5: 'B'})
print("-----")
print(type(curve.items[d]))
print("before call update")
curve.items[d].update(b) # <= DOES NOT WORK AS EXPECTED
print("after call update")
print(curve.items[d]) # MyDict({0.25: 'A', 0.5: 'B'})
session.commit() If you comment out the getset_factory in |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 3 replies
-
so....just looking at this and I don't deal with the assoc proxy stuff very often, the assoc proxy is a "proxy" for a real underlying collection, and the proxy itself doesnt actually implement the storage:
Since your inner dictionary is also an attribute mapped collection, that's based on plain dict so that's the part where you need to inject your sorted dictionary. that is, the dictionary you "see" from the association proxy is always going to be AssociationDict. The "creator" / "getset_factory" stuff deals with a target element that is not also another association proxy, in this case the Item class, so maybe your approach was setting the correct state (not sure) but in any case that's not the dictionary you'd get if you loaded your objects from the database, so i didnt spend much time figuring out what was going wrong there. creating our own dict to be mapped is also a not widely used use case because it can be hard, the example is at https://docs.sqlalchemy.org/en/14/orm/collections.html#custom-dictionary-based-collections. good news, when I just followed the docs exactly, it seems to work and I added a DB round trip plus intentionally unsorting the items to see the sort happens. This is maybe a good example we should add to examples/ . import datetime as dt
from sortedcontainers import SortedDict
from sqlalchemy import Column
from sqlalchemy import create_engine
from sqlalchemy import Date
from sqlalchemy import Float
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.orm.collections import collection
from sqlalchemy.orm.collections import MappedCollection
# sqlalchemy version: 1.3.18
Base = declarative_base()
class MyDict(SortedDict, MappedCollection):
"""custom sorted dictionary stuff"""
def __init__(self):
self.keyfunc = lambda key_value_pair: key_value_pair.key
super().__init__()
@collection.appender
@collection.internally_instrumented
def set(self, value, _sa_initiator=None):
"""Add an item by value, consulting the keyfunc for the key."""
key = self.keyfunc(value)
self.__setitem__(key, value, _sa_initiator)
@collection.remover
@collection.internally_instrumented
def remove(self, value, _sa_initiator=None):
"""Remove an item by value, consulting the keyfunc for the key."""
key = self.keyfunc(value)
# Let self[key] raise if key is not in this collection
# testlib.pragma exempt:__ne__
assert self[key] == value
self.__delitem__(key, _sa_initiator)
def mydict_creator(key, value):
print("creator called")
return Item(item_date=key, series=value)
class Curve(Base):
__tablename__ = "curve"
name = Column(String, primary_key=True)
_items = relationship(
"Item",
collection_class=attribute_mapped_collection("item_date"),
)
items = association_proxy(
target_collection="_items",
attr="series",
creator=mydict_creator,
)
class Item(Base):
__tablename__ = "item"
id = Column(Integer, primary_key=True)
curve_id = Column(Integer, ForeignKey("curve.name"), nullable=False)
item_date = Column(Date, nullable=False)
_series = relationship(
"KeyValuePair",
collection_class=MyDict,
cascade="all, delete, delete-orphan",
)
series = association_proxy(
target_collection="_series",
attr="value",
creator=lambda k, v: KeyValuePair(key=k, value=v),
)
def __init__(self, item_date, series=None):
self.item_date = item_date
if series:
self.series = series
class KeyValuePair(Base):
__tablename__ = "key_value_pair"
id = Column(Integer, primary_key=True)
item_id = Column(Integer, ForeignKey("item.id"), nullable=False)
key = Column(Float)
value = Column(String)
def __init__(self, key, value):
self.key = key
self.value = value
if __name__ == "__main__":
engine = create_engine("sqlite://", echo=False)
Base.metadata.create_all(engine)
session = Session(engine)
# I un-ordered these to see that the ordering occurs
a = {
0.5: "B",
0.25: "A",
}
b = {1.0: "D", 0.5: "C"}
d = dt.date(2021, 12, 31)
curve = session.query(Curve).filter_by(name="curve").one_or_none()
if curve is None:
curve = Curve(name="curve")
curve.items[d] = a
curve.items[d] = a
tmp = curve.items[d]
print(tmp)
print("-----")
print(type(curve.items[d]))
print("before call update")
curve.items[d].update(b)
print("after call update")
print(curve.items[d])
session.add(curve)
session.commit()
session.close()
# reload to see in-python ordering again
curve = session.query(Curve).filter_by(name="curve").one_or_none()
print(curve.items[d]) output, the dict comes out key sorted both times
|
Beta Was this translation helpful? Give feedback.
so....just looking at this and I don't deal with the assoc proxy stuff very often, the assoc proxy is a "proxy" for a real underlying collection, and the proxy itself doesnt actually implement the storage:
Since your inner dictionary is also an attribute mapped collection, that's based on plain dict so that's the part where you need to inject your sorted dictionary. that is, the dictionary you "see" from the association proxy is always going to be AssociationDict. The "creator" / "getset_fac…