From: Mike Bayer Date: Wed, 12 Dec 2018 17:51:20 +0000 (-0500) Subject: Document and support nested composites X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b7ad656e06a1fe64a60e13540f3810d98ffb151e;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git 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 d4a130bb1b92869efe33675262c7b1fde364e477) --- diff --git a/doc/build/orm/composites.rst b/doc/build/orm/composites.rst index 43521ac991..71b1294330 100644 --- a/doc/build/orm/composites.rst +++ b/doc/build/orm/composites.rst @@ -158,3 +158,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) \ No newline at end of file diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index bc9c7f97ec..82e60cb6be 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -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. diff --git a/test/orm/test_composites.py b/test/orm/test_composites.py index 375dcb77a2..518b901453 100644 --- a/test/orm/test_composites.py +++ b/test/orm/test_composites.py @@ -313,6 +313,80 @@ class PointTest(fixtures.MappedTest): 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):