From 0868f2aaef9c594682dffbb48511e62b62f2218f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 9 Sep 2020 11:40:04 +0200 Subject: [PATCH] #423 - Add r2dbc-postgresql Geotypes to simple types. R2DBC Postgres Geo-types are now considered simple types that are passed-thru to the driver without further mapping. Types such as io.r2dbc.postgresql.codec.Circle or io.r2dbc.postgresql.codec.Box can be used directly in domain models and as bind parameters. --- pom.xml | 4 +- .../data/r2dbc/dialect/PostgresDialect.java | 170 +++++++++++++++++- .../r2dbc/core/PostgresIntegrationTests.java | 85 ++++++++- .../r2dbc/testing/PostgresTestSupport.java | 2 +- 4 files changed, 252 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index 5d98456af..5e26beb59 100644 --- a/pom.xml +++ b/pom.xml @@ -35,7 +35,7 @@ 0.8.0.RELEASE 7.1.2.jre8-preview 2.5.4 - Arabba-SR6 + Arabba-BUILD-SNAPSHOT 1.0.3 4.1.47.Final @@ -221,7 +221,7 @@ io.r2dbc r2dbc-postgresql - test + true diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java index 77f838ee2..c6e0d0c99 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java @@ -3,15 +3,28 @@ package org.springframework.data.r2dbc.dialect; import java.net.InetAddress; import java.net.URI; import java.net.URL; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.geo.Box; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.Point; +import org.springframework.data.geo.Polygon; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.relational.core.dialect.ArrayColumns; import org.springframework.data.util.Lazy; +import org.springframework.lang.NonNull; import org.springframework.r2dbc.core.binding.BindMarkersFactory; import org.springframework.util.ClassUtils; @@ -25,15 +38,25 @@ public class PostgresDialect extends org.springframework.data.relational.core.di private static final Set> SIMPLE_TYPES; + private static final boolean GEO_TYPES_PRESENT = ClassUtils.isPresent("io.r2dbc.postgresql.codec.Polygon", + PostgresDialect.class.getClassLoader()); + static { Set> simpleTypes = new HashSet<>(Arrays.asList(UUID.class, URL.class, URI.class, InetAddress.class)); - if (ClassUtils.isPresent("io.r2dbc.postgresql.codec.Json", PostgresDialect.class.getClassLoader())) { + // conditional Postgres JSON support. + ifClassPresent("io.r2dbc.postgresql.codec.Json", simpleTypes::add); - simpleTypes - .add(ClassUtils.resolveClassName("io.r2dbc.postgresql.codec.Json", PostgresDialect.class.getClassLoader())); - } + // conditional Postgres Geo support. + Stream.of("io.r2dbc.postgresql.codec.Box", // + "io.r2dbc.postgresql.codec.Circle", // + "io.r2dbc.postgresql.codec.Line", // + "io.r2dbc.postgresql.codec.Lseg", // + "io.r2dbc.postgresql.codec.Point", // + "io.r2dbc.postgresql.codec.Path", // + "io.r2dbc.postgresql.codec.Polygon") // + .forEach(s -> ifClassPresent(s, simpleTypes::add)); SIMPLE_TYPES = simpleTypes; } @@ -76,6 +99,23 @@ public class PostgresDialect extends org.springframework.data.relational.core.di return this.arrayColumns.get(); } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.Dialect#getConverters() + */ + @Override + public Collection getConverters() { + + if (GEO_TYPES_PRESENT) { + return Arrays.asList(FromPostgresPointConverter.INSTANCE, ToPostgresPointConverter.INSTANCE, // + FromPostgresCircleConverter.INSTANCE, ToPostgresCircleConverter.INSTANCE, // + FromPostgresBoxConverter.INSTANCE, ToPostgresBoxConverter.INSTANCE, // + FromPostgresPolygonConverter.INSTANCE, ToPostgresPolygonConverter.INSTANCE); + } + + return Collections.emptyList(); + } + private static class R2dbcArrayColumns implements ArrayColumns { private final ArrayColumns delegate; @@ -107,4 +147,126 @@ public class PostgresDialect extends org.springframework.data.relational.core.di } } + /** + * If the class is present on the class path, invoke the specified consumer {@code action} with the class object, + * otherwise do nothing. + * + * @param action block to be executed if a value is present. + */ + private static void ifClassPresent(String className, Consumer> action) { + + if (ClassUtils.isPresent(className, PostgresDialect.class.getClassLoader())) { + action.accept(ClassUtils.resolveClassName(className, PostgresDialect.class.getClassLoader())); + } + } + + @ReadingConverter + private enum FromPostgresBoxConverter implements Converter { + + INSTANCE; + + @Override + public Box convert(io.r2dbc.postgresql.codec.Box source) { + return new Box(FromPostgresPointConverter.INSTANCE.convert(source.getA()), + FromPostgresPointConverter.INSTANCE.convert(source.getB())); + } + } + + @WritingConverter + private enum ToPostgresBoxConverter implements Converter { + + INSTANCE; + + @Override + public io.r2dbc.postgresql.codec.Box convert(Box source) { + return io.r2dbc.postgresql.codec.Box.of(ToPostgresPointConverter.INSTANCE.convert(source.getFirst()), + ToPostgresPointConverter.INSTANCE.convert(source.getSecond())); + } + } + + @ReadingConverter + private enum FromPostgresCircleConverter implements Converter { + + INSTANCE; + + @Override + public Circle convert(io.r2dbc.postgresql.codec.Circle source) { + return new Circle(source.getCenter().getX(), source.getCenter().getY(), source.getRadius()); + } + } + + @WritingConverter + private enum ToPostgresCircleConverter implements Converter { + + INSTANCE; + + @Override + public io.r2dbc.postgresql.codec.Circle convert(Circle source) { + return io.r2dbc.postgresql.codec.Circle.of(source.getCenter().getX(), source.getCenter().getY(), + source.getRadius().getValue()); + } + } + + @ReadingConverter + private enum FromPostgresPolygonConverter implements Converter { + + INSTANCE; + + @Override + public Polygon convert(io.r2dbc.postgresql.codec.Polygon source) { + + List sourcePoints = source.getPoints(); + List targetPoints = new ArrayList<>(sourcePoints.size()); + + for (io.r2dbc.postgresql.codec.Point sourcePoint : sourcePoints) { + targetPoints.add(FromPostgresPointConverter.INSTANCE.convert(sourcePoint)); + } + + return new Polygon(targetPoints); + } + } + + @WritingConverter + private enum ToPostgresPolygonConverter implements Converter { + + INSTANCE; + + @Override + public io.r2dbc.postgresql.codec.Polygon convert(Polygon source) { + + List sourcePoints = source.getPoints(); + List targetPoints = new ArrayList<>(sourcePoints.size()); + + for (Point sourcePoint : sourcePoints) { + targetPoints.add(ToPostgresPointConverter.INSTANCE.convert(sourcePoint)); + } + + return io.r2dbc.postgresql.codec.Polygon.of(targetPoints); + } + } + + @ReadingConverter + private enum FromPostgresPointConverter implements Converter { + + INSTANCE; + + @Override + @NonNull + public Point convert(io.r2dbc.postgresql.codec.Point source) { + return new Point(source.getX(), source.getY()); + } + } + + @WritingConverter + private enum ToPostgresPointConverter implements Converter { + + INSTANCE; + + @Override + @NonNull + public io.r2dbc.postgresql.codec.Point convert(Point source) { + return io.r2dbc.postgresql.codec.Point.of(source.getX(), source.getY()); + } + } + } diff --git a/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java index 78f7c9e11..3b9cf12e0 100644 --- a/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java @@ -20,7 +20,14 @@ import static org.springframework.data.relational.core.query.Criteria.*; import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; import io.r2dbc.postgresql.PostgresqlConnectionFactory; +import io.r2dbc.postgresql.codec.Box; +import io.r2dbc.postgresql.codec.Circle; import io.r2dbc.postgresql.codec.EnumCodec; +import io.r2dbc.postgresql.codec.Line; +import io.r2dbc.postgresql.codec.Lseg; +import io.r2dbc.postgresql.codec.Path; +import io.r2dbc.postgresql.codec.Point; +import io.r2dbc.postgresql.codec.Polygon; import io.r2dbc.postgresql.extension.CodecRegistrar; import io.r2dbc.spi.ConnectionFactory; import lombok.AllArgsConstructor; @@ -36,7 +43,6 @@ import javax.sql.DataSource; import org.junit.Before; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Test; import org.springframework.dao.DataAccessException; @@ -133,7 +139,6 @@ public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport { } @Test // gh-411 - @Ignore("Depends on https://github.com/pgjdbc/r2dbc-postgresql/issues/301") public void shouldWriteAndReadEnumValuesUsingDriverInternals() { CodecRegistrar codecRegistrar = EnumCodec.builder().withEnum("state_enum", State.class).build(); @@ -183,6 +188,63 @@ public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport { } + @Test // gh-423 + public void shouldReadAndWriteGeoTypes() { + + GeoType geoType = new GeoType(); + geoType.thePoint = Point.of(1, 2); + geoType.theBox = Box.of(Point.of(3, 4), Point.of(1, 2)); + geoType.theCircle = Circle.of(1, 2, 3); + geoType.theLine = Line.of(1, 2, 3, 4); + geoType.theLseg = Lseg.of(Point.of(1, 2), Point.of(3, 4)); + geoType.thePath = Path.open(Point.of(1, 2), Point.of(3, 4)); + geoType.thePolygon = Polygon.of(Point.of(1, 2), Point.of(3, 4), Point.of(5, 6), Point.of(1, 2)); + geoType.springDataBox = new org.springframework.data.geo.Box(new org.springframework.data.geo.Point(3, 4), + new org.springframework.data.geo.Point(1, 2)); + geoType.springDataCircle = new org.springframework.data.geo.Circle(1, 2, 3); + geoType.springDataPoint = new org.springframework.data.geo.Point(1, 2); + geoType.springDataPolygon = new org.springframework.data.geo.Polygon(new org.springframework.data.geo.Point(1, 2), + new org.springframework.data.geo.Point(3, 4), new org.springframework.data.geo.Point(5, 6), + new org.springframework.data.geo.Point(1, 2)); + + template.execute("DROP TABLE IF EXISTS geo_type"); + template.execute("CREATE TABLE geo_type (" // + + "id serial PRIMARY KEY," // + + "the_point POINT," // + + "the_box BOX," // + + "the_circle CIRCLE," // + + "the_line LINE," // + + "the_lseg LSEG," // + + "the_path PATH," // + + "the_polygon POLYGON," // + + "spring_data_box BOX," // + + "spring_data_circle CIRCLE," // + + "spring_data_point POINT," // + + "spring_data_polygon POLYGON" // + + ")"); + + R2dbcEntityTemplate template = new R2dbcEntityTemplate(client, + new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE)); + + GeoType saved = template.insert(geoType).block(); + GeoType loaded = template.select(Query.empty(), GeoType.class) // + .blockLast(); + + assertThat(saved.id).isEqualTo(loaded.id); + assertThat(saved.thePoint).isEqualTo(loaded.thePoint); + assertThat(saved.theBox).isEqualTo(loaded.theBox); + assertThat(saved.theCircle).isEqualTo(loaded.theCircle); + assertThat(saved.theLine).isEqualTo(loaded.theLine); + assertThat(saved.theLseg).isEqualTo(loaded.theLseg); + assertThat(saved.thePath).isEqualTo(loaded.thePath); + assertThat(saved.thePolygon).isEqualTo(loaded.thePolygon); + assertThat(saved.springDataBox).isEqualTo(loaded.springDataBox); + assertThat(saved.springDataCircle).isEqualTo(loaded.springDataCircle); + assertThat(saved.springDataPoint).isEqualTo(loaded.springDataPoint); + assertThat(saved.springDataPolygon).isEqualTo(loaded.springDataPolygon); + assertThat(saved).isEqualTo(loaded); + } + private void insert(EntityWithArrays object) { client.insert() // @@ -219,4 +281,23 @@ public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport { int[][] multidimensionalArray; List collectionArray; } + + @Data + static class GeoType { + + @Id Integer id; + + Point thePoint; + Box theBox; + Circle theCircle; + Line theLine; + Lseg theLseg; + Path thePath; + Polygon thePolygon; + + org.springframework.data.geo.Box springDataBox; + org.springframework.data.geo.Circle springDataCircle; + org.springframework.data.geo.Point springDataPoint; + org.springframework.data.geo.Polygon springDataPolygon; + } } diff --git a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java index 5f68e9ea3..26cd2d628 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java @@ -81,7 +81,7 @@ public class PostgresTestSupport { .database("postgres") // .username("postgres") // .password("") // - .jdbcUrl("jdbc:postgresql://localhost/postgres") // + .jdbcUrl("jdbc:postgresql://localhost:5432/postgres") // .build(); }