]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Document and support nested composites
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 12 Dec 2018 17:51:20 +0000 (12:51 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 12 Dec 2018 17:56:57 +0000 (12:56 -0500)
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)

doc/build/orm/composites.rst
lib/sqlalchemy/orm/descriptor_props.py
test/orm/test_composites.py

index 43521ac9913a784917ef0e8803e8d664296ca5e9..71b1294330fda097fd6a8f25ea712e032c89b040 100644 (file)
@@ -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
index bc9c7f97ec617bd2f7395aabc0744ca03842ef08..82e60cb6be1d338be517f16f71291b39809c3c09 100644 (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.
index 375dcb77a2f4727bf286451eef625e473266b0eb..518b901453839b3775528f77d785542f2d107484 100644 (file)
@@ -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):