Document and support nested composites

Composites can behave in a "nested" fashion by defining the
class in that way.   To make the constructor more convenient,
a callable can be passed to :func:`.composite` instead of the
class itself.  This works now, so add a test to ensure this
pattern remains available.

Change-Id: Ia009f274fca7269f41d6d824e0f70b6fb0ada081
(cherry picked from commit d4a130bb1b)
This commit is contained in:
Mike Bayer
2018-12-12 12:51:20 -05:00
parent 560452acd2
commit eb73b1a1cd
3 changed files with 148 additions and 1 deletions
+70
View File
@@ -148,3 +148,73 @@ the same expression that the base "greater than" does::
end = composite(Point, x2, y2,
comparator_factory=PointComparator)
Nesting Composites
-------------------
Composite objects can be defined to work in simple nested schemes, by
redefining behaviors within the composite class to work as desired, then
mapping the composite class to the full length of individual columns normally.
Typically, it is convenient to define separate constructors for user-defined
use and generate-from-row use. Below we reorganize the ``Vertex`` class to
itself be a composite object, which is then mapped to a class ``HasVertex``::
from sqlalchemy.orm import composite
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __composite_values__(self):
return self.x, self.y
def __repr__(self):
return "Point(x=%r, y=%r)" % (self.x, self.y)
def __eq__(self, other):
return isinstance(other, Point) and \
other.x == self.x and \
other.y == self.y
def __ne__(self, other):
return not self.__eq__(other)
class Vertex(object):
def __init__(self, start, end):
self.start = start
self.end = end
@classmethod
def _generate(self, x1, y1, x2, y2):
"""generate a Vertex from a row"""
return Vertex(
Point(x1, y1),
Point(x2, y2)
)
def __composite_values__(self):
return \
self.start.__composite_values__() + \
self.end.__composite_values__()
class HasVertex(Base):
__tablename__ = 'has_vertex'
id = Column(Integer, primary_key=True)
x1 = Column(Integer)
y1 = Column(Integer)
x2 = Column(Integer)
y2 = Column(Integer)
vertex = composite(Vertex._generate, x1, y1, x2, y2)
We can then use the above mapping as::
hv = HasVertex(vertex=Vertex(Point(1, 2), Point(3, 4)))
s.add(hv)
s.commit()
hv = s.query(HasVertex).filter(
HasVertex.vertex == Vertex(Point(1, 2), Point(3, 4))).first()
print(hv.vertex.start)
print(hv.vertex.end)
+3 -1
View File
@@ -100,7 +100,9 @@ class CompositeProperty(DescriptorProperty):
is the :class:`.CompositeProperty`.
:param class\_:
The "composite type" class.
The "composite type" class, or any classmethod or callable which
will produce a new instance of the composite object given the
column values in order.
:param \*cols:
List of Column objects to be mapped.
+75
View File
@@ -11,6 +11,8 @@ from sqlalchemy.testing import eq_
from sqlalchemy.testing import fixtures
class PointTest(fixtures.MappedTest, testing.AssertsCompiledSQL):
@classmethod
def define_tables(cls, metadata):
@@ -365,6 +367,79 @@ class PointTest(fixtures.MappedTest, testing.AssertsCompiledSQL):
eq_(e.start, None)
class NestedTest(fixtures.MappedTest, testing.AssertsCompiledSQL):
@classmethod
def define_tables(cls, metadata):
Table('stuff', metadata,
Column('id', Integer, primary_key=True,
test_needs_autoincrement=True),
Column("a", String(30)),
Column("b", String(30)),
Column("c", String(30)),
Column("d", String(30)))
def _fixture(self):
class AB(object):
def __init__(self, a, b, cd):
self.a = a
self.b = b
self.cd = cd
@classmethod
def generate(cls, a, b, c, d):
return AB(a, b, CD(c, d))
def __composite_values__(self):
return (self.a, self.b) + self.cd.__composite_values__()
def __eq__(self, other):
return isinstance(other, AB) and \
self.a == other.a and self.b == other.b and \
self.cd == other.cd
def __ne__(self, other):
return not self.__eq__(other)
class CD(object):
def __init__(self, c, d):
self.c = c
self.d = d
def __composite_values__(self):
return (self.c, self.d)
def __eq__(self, other):
return isinstance(other, CD) and \
self.c == other.c and self.d == other.d
def __ne__(self, other):
return not self.__eq__(other)
class Thing(object):
def __init__(self, ab):
self.ab = ab
stuff = self.tables.stuff
mapper(Thing, stuff, properties={
"ab": composite(
AB.generate, stuff.c.a, stuff.c.b, stuff.c.c, stuff.c.d)
})
return Thing, AB, CD
def test_round_trip(self):
Thing, AB, CD = self._fixture()
s = Session()
s.add(Thing(AB('a', 'b', CD('c', 'd'))))
s.commit()
s.close()
t1 = s.query(Thing).filter(
Thing.ab == AB('a', 'b', CD('c', 'd'))).one()
eq_(t1.ab, AB('a', 'b', CD('c', 'd')))
class PrimaryKeyTest(fixtures.MappedTest):
@classmethod
def define_tables(cls, metadata):