Map a PostGIS geometry point field with Hibernate on Spring Boot - spring

In my PostgreSQL 9.3 + PostGIS 2.1.5 I have a table PLACE with a column coordinates of type Geometry(Point,26910).
I want to map it to Place entity in my Spring Boot 1.1.9 web application, which uses Hibernate 4.0.0 + . Place is available with a REST repository.
Unfortunately when I GET http://localhost:8080/mywebapp/places I receive this strange JSON response:
{
"_embedded" : {
"venues" : [ {
"id" : 1,
"coordinates" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
"envelope" : {
and so on indefinetely...! Spring log doesn't help..
I'm working with this application.properties:
spring.jpa.database-platform=org.hibernate.spatial.dialect.postgis.PostgisDialect
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:postgresql://192.168.1.123/mywebapp
spring.datasource.username=postgres
spring.datasource.password=mypwd
spring.datasource.driverClassName=org.postgresql.Driver
First of all, is it ok to use database-platform instead of database?
And maybe do I have to use following settings instead of the above?
spring.datasource.url=jdbc:postgresql_postGIS://192.168.1.123/mywebapp
spring.datasource.driverClassName=org.postgis.DriverWrapper
Anyway my entity is something like this:
#Entity
public class Place {
#Id
public int id;
#Column(columnDefinition="Geometry")
#Type(type="org.hibernate.spatial.GeometryType") //"org.hibernatespatial.GeometryUserType" seems to be for older versions of Hibernate Spatial
public com.vividsolutions.jts.geom.Point coordinates;
}
My pom.xml contains this relevant part:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.3-1102-jdbc41</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-spatial</artifactId>
<version>4.3</version><!-- compatible with Hibernate 4.3.x -->
<exclusions>
<exclusion>
<artifactId>postgresql</artifactId>
<groupId>postgresql</groupId>
</exclusion>
</exclusions>
</dependency>
A bit strange configuration, I found it on the internet, it is the one that works best for now.
I hope that someone could help me with this mistery. :)

Finally I discovered that my configuration is ok and might be Jackson that cannot manage Point data type correctly. So I customized its JSON serialization and deserialization:
add these annotations to our coordinates field:
#JsonSerialize(using = PointToJsonSerializer.class)
#JsonDeserialize(using = JsonToPointDeserializer.class)
create such serializer:
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.vividsolutions.jts.geom.Point;
public class PointToJsonSerializer extends JsonSerializer<Point> {
#Override
public void serialize(Point value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
String jsonValue = "null";
try
{
if(value != null) {
double lat = value.getY();
double lon = value.getX();
jsonValue = String.format("POINT (%s %s)", lat, lon);
}
}
catch(Exception e) {}
jgen.writeString(jsonValue);
}
}
create such deserializer:
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.PrecisionModel;
public class JsonToPointDeserializer extends JsonDeserializer<Point> {
private final static GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 26910);
#Override
public Point deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
try {
String text = jp.getText();
if(text == null || text.length() <= 0)
return null;
String[] coordinates = text.replaceFirst("POINT ?\\(", "").replaceFirst("\\)", "").split(" ");
double lat = Double.parseDouble(coordinates[0]);
double lon = Double.parseDouble(coordinates[1]);
Point point = geometryFactory.createPoint(new Coordinate(lat, lon));
return point;
}
catch(Exception e){
return null;
}
}
}
Maybe you can also use this serializer and this deserializer, available here.

The solutions above helped me to fix the problem. I simplify it so other people can understand.
I included this library in my pom.xml:
<dependency>
<groupId>com.bedatadriven</groupId>
<artifactId>jackson-datatype-jts</artifactId>
<version>2.2</version>
</dependency>
This is the POJO object I used. Then I was able to get the REST call to work without the envelope error and proper coordinates.
import com.bedatadriven.jackson.datatype.jts.serialization.GeometryDeserializer;
import com.bedatadriven.jackson.datatype.jts.serialization.GeometrySerializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.vividsolutions.jts.geom.Geometry;
#Entity
#Table(name = "boundary")
public class Boundary {
private int id;
private Geometry geometry;
#Id
public int getId() {
return ogc_fid;
}
public void setId(int id) {
this.id = id;
}
#JsonSerialize(using = GeometrySerializer.class)
#JsonDeserialize(using = GeometryDeserializer.class)
#Column(name = "geometry", columnDefinition = "Geometry")
public Geometry getGeometry() {
return geometry;
}
public void setGeometry(Geometry geometry) {
this.geometry = geometry;
}
}
My table had these 2 columns:
id | integer
geometry | geometry(Geometry,4326) |

With Hibernate 5.4 and Posgis 13+ its became very easy. We can just add latest hibernate-spatial dependency
<!-- Hibernate Spatial for storing and retrieving geometries -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-spatial</artifactId>
<version>${hibernate.version}</version>
</dependency>
Use latest Posgis dialect. Add in application.properties:
spring.jpa.properties.hibernate.dialect=org.hibernate.spatial.dialect.postgis.PostgisPG95Dialect
Then we can simply use JST geometry because its supported by Hibernate (see here).
import org.locationtech.jts.geom.Point;
#Entity(name = "address")
public class Address {
private Point location;
}
SQL to create entity table :
CREATE TABLE if not EXISTS address (location geometry);
Further JTS GeoJSON Reader and Writer can be used to translate geometries into JSON (see here)

This serialization/deserialization also worked fine for me.
https://github.com/bedatadriven/jackson-datatype-jts

The problem doesn't appear to be related to PostgreSQL. It appears that your POJO has a backreference, which means that your mapper doesn't know how to handle it. You need to explicitly define the recursive relationships so that the mapper knows when to stop. (My Goto link --> http://vard-lokkur.blogspot.com/2010/10/json-jackson-to-rescue.html)

If you don't want to add the annotation on all your fields that are using a Point, you can also use the #JsonComponent to register your JsonSerializer and JsonDeserializer.
#JsonComponent
public class PointSerializer extends JsonSerializer<com.vividsolutions.jts.geom.Point>{
#Override
public void serialize(com.vividsolutions.jts.geom.Point value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeNumberField("lat", value.getY());
gen.writeNumberField("lon", value.getX());
gen.writeEndObject();
}
}

Related

Optimistic locking Test in spring data JPA

I'm trying to test the optimistic locking mechanism in spring data jpa by loading a certain entity twice using findBy function, then updating the first one and asserting that when the second one is updated, it will throw an OptimisticLockingFailureException.
But the problem is that no exception is thrown and the second update is done successfully.
After investigation i found that findBy function hits the database only the first time and caches the returned entity. and when i call it again it returns cached entity. which means that both loaded entities are equal. so the first update reflects in both entities making the second entity does not have the stale data.
so, how do i force loading the second entity from the data base in the second findBy function call ?
Here is my code:-
Test class
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
#DataJpaTest
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class PersistenceTests {
#Autowired
private ProductRepository repository;
private ProductEntity savedEntity;
#DynamicPropertySource
static void databaseProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", () -> "jdbc:mysql://localhost:3306/code_snippet");
registry.add("spring.datasource.username", () -> "root");
registry.add("spring.datasource.password", () -> "System");
registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop");
}
#BeforeEach
void setupDb() {
repository.deleteAll();
ProductEntity entity = new ProductEntity(1, "n", 1);
savedEntity = repository.save(entity);
assertEqualsProduct(entity, savedEntity);
}
#Test
void optimisticLockError() {
// Store the saved entity in two separate entity objects
ProductEntity entity1 = repository.findById(savedEntity.getId()).get();
ProductEntity entity2 = repository.findById(savedEntity.getId()).get();
// Update the entity using the first entity object
entity1.setName("n1");
repository.save(entity1);
// Update the entity using the second entity object.
// This should fail since the second entity now holds an old version number,
// i.e. an Optimistic Lock Error
assertThrows(OptimisticLockingFailureException.class, () -> {
entity2.setName("n2");
repository.save(entity2);
});
// Get the updated entity from the database and verify its new sate
ProductEntity updatedEntity = repository.findById(savedEntity.getId()).get();
assertEquals(1, (int) updatedEntity.getVersion());
assertEquals("n1", updatedEntity.getName());
}
private void assertEqualsProduct(ProductEntity expectedEntity, ProductEntity actualEntity) {
assertEquals(expectedEntity.getId(), actualEntity.getId());
assertEquals(expectedEntity.getVersion(), actualEntity.getVersion());
assertEquals(expectedEntity.getProductId(), actualEntity.getProductId());
assertEquals(expectedEntity.getName(), actualEntity.getName());
assertEquals(expectedEntity.getWeight(), actualEntity.getWeight());
}
}
Entity
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;
#Entity
#Table(name = "product")
public class ProductEntity {
#Id
#GeneratedValue
private Integer id;
#Version
private Integer version;
private int productId;
private String name;
private int weight;
public ProductEntity() {
}
public ProductEntity(int productId, String name, int weight) {
this.productId = productId;
this.name = name;
this.weight = weight;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
}
public int getProductId() {
return productId;
}
public void setProductId(int productId) {
this.productId = productId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
}
Repository
import java.util.Optional;
import org.springframework.data.repository.PagingAndSortingRepository;
public interface ProductRepository extends PagingAndSortingRepository<ProductEntity, Integer> {
Optional<ProductEntity> findByProductId(int productId);
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.javaworld.codesnippet</groupId>
<artifactId>writing-persistence-tests</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>writing-persistence-tests</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Main Class
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class WritingPersistenceTestsApplication {
public static void main(String[] args) {
SpringApplication.run(WritingPersistenceTestsApplication.class, args);
}
}
The problem is that your test method is by default transactional. You can disable the transactional for this method by adding:
#Test
#Transactional(value = Transactional.TxType.NEVER)
Than you get in the second save ObjectOptimisticLockingFailureException

How to make spring boot, spatial hibernate and postgis work?

I can't start my spring boot(2.6.3) project with hibernate-spatial in create mode.
It tells me that type "geometry does not exist".
The geometry type comes from the hibernate-spatial library.
However, I applied everything necessary:
add hibernate-spatial dependency (my version 5.6.3.Final)
use the org.hibernate.spatial.dialect.postgis.PostgisDialect dialect
Moreover this class is deprecated and the documentation corresponding to the same version, it still indicates to use it, I do not understand anything (https://docs.jboss.org/hibernate/orm/5.6/userguide /html_single/Hibernate_User_Guide.html#spatial)
Use Geometry type from geolatte group or jts group
Despite that I have an error where it cannot create the table because the type "geometry does not exist".
Here are my maven dependencies:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-spatial</artifactId>
<version>5.6.3.Final</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.1</version>
</dependency>
<!--<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.18.2</version>
</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
My properties :
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/postgres?currentSchema=hibernatespatial
username: postgres
password:
jpa:
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate:
dialect: org.hibernate.spatial.dialect.postgis.PostgisDialect
open-in-view: false
database-platform: org.hibernate.spatial.dialect.postgis.PostgisDialect
My entity class :
package org.test.hibernate.spatial;
import org.geolatte.geom.Geometry;
import javax.persistence.*;
#Entity
#Table
public class Person {
#Id
#GeneratedValue
private Long id;
private String name;
private String lastname;
private String age;
private Geometry geom;
public Geometry getGeom() {
return geom;
}
public void setGeom(Geometry geom) {
this.geom = geom;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
My repository class :
package org.test.hibernate.spatial;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PersonRepository extends JpaRepository<Person, Long> {
}
My boot class :
package org.test.hibernate.spatial;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
#SpringBootApplication
#EnableJpaRepositories
#EnableTransactionManagement
public class TestHibernateSpatialApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(TestHibernateSpatialApplication.class, args);
}
public void run(String... args) throws Exception {
}
}
My postgreSQL database is 14 version.
Somebody have any idea what is wrong ?
PostGIS is a Postgres extension, which needs to be enabled for each database:
Once PostGIS is installed, it needs to be enabled (Section 3.3, “Creating spatial databases”) or upgraded (Section 3.4, “Upgrading spatial databases”) in each individual database you want to use it in.
[...]
Run the following SQL snippet in the database you want to enable spatially:
CREATE EXTENSION IF NOT EXISTS plpgsql;
CREATE EXTENSION postgis;
Also be aware that the extension is by default installed to the default schema (e.g. public). So when using the currentSchema option, be sure to not accidentally exclude the schema postgis was installed into. To prevent this, one could either add the postgis schema to the currentSchema (e.g. jdbc:postgresql://localhost:5432/tst?currentSchema=app1,public), or move postgis to the preferred schema.

Transaction support in spring-boot running Tomcat, with AspectJ load-time weaving (LTW)

I am having a hard time configuring transactional support in spring-boot 2.0.3 with AspectJ LTW (load-time weaving). My spring-boot is running embedded Tomcat servlet container. In my persistence layer, I am not using JPA, but Spring JDBC Template instead.
I opted for AspectJ mode for transaction management because we are leveraging a rather big project with nested transactions and sometimes it is hard to keep track of all the applications of #Transactional annotation. So that when this annotation is being used I want to have a predictable result - atomic DB operation. I do not want to think about whether we have a self-invocation or method that is marked to be transactional is public.
I have read a bunch of documentation regarding transaction support in spring and how to configure LTW AspectJ weaving.
Unfortunately, I cannot make it work. I have created a test (spring-boot test class) that is meant to mimic different failures in a code that should be transactional (see it below). Also, I cannot see the weaving actually happening. I am clearly missing something, cannot figure out what.
My test class:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = TestConfig.class)
#ActiveProfiles("TEST")
public class TransactionalIT {
#SpyBean
private JdbcTemplate jdbcTemplate;
// we need this guy in order to perform a cleanup in static #AfterClass method
private static JdbcTemplate jdbcTemplateInStaticContext;
#Autowired
private PlatformTransactionManager txManager;
#Spy
private NestedTransactionsJdbcDao dao;
#Before
public void setUp() {
if (jdbcTemplateInStaticContext == null) {
// Making sure we're working with the proper tx manager
assertThat(txManager).isNotNull();
assertThat(txManager.getClass()).isEqualTo(DataSourceTransactionManager.class);
jdbcTemplateInStaticContext = jdbcTemplate;
jdbcTemplateInStaticContext.execute("CREATE TABLE entity_a (id varchar(12) PRIMARY KEY, name varchar(24), description varchar(255));");
jdbcTemplateInStaticContext.execute("CREATE TABLE entity_b (id varchar(12) PRIMARY KEY, name varchar(24), description varchar(255));");
jdbcTemplateInStaticContext.execute("CREATE TABLE entity_a_to_b_assn (entity_a_id varchar(12) NOT NULL, entity_b_id varchar(12) NOT NULL, " +
"CONSTRAINT fk_entity_a FOREIGN KEY (entity_a_id) REFERENCES entity_a(id), " +
"CONSTRAINT fk_entity_b FOREIGN KEY (entity_b_id) REFERENCES entity_b(id), " +
"UNIQUE (entity_a_id, entity_b_id));");
}
}
#AfterClass
public static void cleanup() {
if (jdbcTemplateInStaticContext != null) {
jdbcTemplateInStaticContext.execute("DROP TABLE entity_a_to_b_assn;");
jdbcTemplateInStaticContext.execute("DROP TABLE entity_a;");
jdbcTemplateInStaticContext.execute("DROP TABLE entity_b;");
}
}
#Test
public void createObjectGraph_FailsDuring_AnAttemptToCreate3rdEntityA() {
doThrow(new RuntimeException("blah!")).when(jdbcTemplate).update(eq("INSERT INTO entity_a (id, name, description) VALUES(?, ?, ?);"),
eq("a3"), eq("entity a3"), eq("descr_a_3"));
try {
dao.createObjectGraph(getObjectGraph());
fail("Should never reach this point");
} catch (RuntimeException e) {
assertThat(e.getMessage()).isEqualTo("blah!");
assertDbCounts(0L, 0L, 0L);
}
}
private void assertDbCounts(long expectedACount, long expectedBCount, long expectedAToBCount) {
Long actualACount = jdbcTemplate.queryForObject("SELECT count(*) count_a FROM entity_a", new LongRowMapper());
assertThat(actualACount).isEqualTo(expectedACount);
Long actualBCount = jdbcTemplate.queryForObject("SELECT count(*) count_b FROM entity_b", new LongRowMapper());
assertThat(actualBCount).isEqualTo(expectedBCount);
Long actualAToBCount = jdbcTemplate.queryForObject("SELECT count(*) count_a_to_b FROM entity_b", new LongRowMapper());
assertThat(actualAToBCount).isEqualTo(expectedAToBCount);
}
private final class LongRowMapper implements RowMapper<Long> {
#Override
public Long mapRow(ResultSet resultSet, int i) throws SQLException {
return resultSet.getLong(1);
}
}
private ObjectGraph getObjectGraph() {
EntityA a1 = new EntityA("a1", "entity a1", "descr_a_1");
EntityA a2 = new EntityA("a2", "entity a2", "descr_a_2");
EntityA a3 = new EntityA("a3", "entity a3", "descr_a_3");
EntityB b1 = new EntityB("b1", "entity b1", "descr_b_1");
EntityB b2 = new EntityB("b2", "entity b2", "descr_b_2");
EntityB b3 = new EntityB("b3", "entity b3", "descr_b_3");
AtoBAssn a1b1 = new AtoBAssn("a1", "b1");
AtoBAssn a1b3 = new AtoBAssn("a1", "b3");
AtoBAssn a2b2 = new AtoBAssn("a2", "b2");
AtoBAssn a2b3 = new AtoBAssn("a2", "b3");
AtoBAssn a3b1 = new AtoBAssn("a3", "b1");
return new ObjectGraph(
Lists.newArrayList(a1, a2, a3),
Lists.newArrayList(b1, b2, b3),
Lists.newArrayList(a1b1, a1b3, a2b2, a2b3, a3b1));
}
#Data
#AllArgsConstructor
private class EntityA {
private String id;
private String name;
private String description;
}
#Data
#AllArgsConstructor
private class EntityB {
private String id;
private String name;
private String description;
}
#Data
#AllArgsConstructor
private class AtoBAssn {
private String idA;
private String idB;
}
#Data
#AllArgsConstructor
private class ObjectGraph {
private List<EntityA> aList;
private List<EntityB> bList;
List<AtoBAssn> aToBAssnList;
}
#Repository
public class NestedTransactionsJdbcDao {
#Transactional
public void createObjectGraph(ObjectGraph og) {
createEntitiesA(og.getAList());
createEntitiesB(og.getBList());
createAtoBAssn(og.getAToBAssnList());
doSomethingElse();
}
#Transactional
public void createEntitiesA(List<EntityA> aList) {
aList.forEach(a ->
jdbcTemplate.update("INSERT INTO entity_a (id, name, description) VALUES(?, ?, ?);",
a.getId(), a.getName(), a.getDescription()));
}
#Transactional
public void createEntitiesB(List<EntityB> bList) {
bList.forEach(b ->
jdbcTemplate.update("INSERT INTO entity_b (id, name, description) VALUES(?, ?, ?);",
b.getId(), b.getName(), b.getDescription()));
}
#Transactional
/**
* Intentionally access is set to package-private
*/
void createAtoBAssn(List<AtoBAssn> aToBAssnList) {
aToBAssnList.forEach(aToB ->
jdbcTemplate.update("INSERT INTO entity_a_to_b_assn (entity_a_id, entity_b_id) VALUES(?, ?);",
aToB.getIdA(), aToB.getIdB()));
}
void doSomethingElse() {
// Intentionally left blank
}
}
}
Here is my configuration class:
import org.apache.catalina.loader.WebappClassLoader;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableLoadTimeWeaving;
import org.springframework.context.annotation.Primary;
import org.springframework.instrument.classloading.LoadTimeWeaver;
import org.springframework.instrument.classloading.tomcat.TomcatLoadTimeWeaver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.aspectj.AnnotationTransactionAspect;
#Configuration
#EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
#EnableLoadTimeWeaving(aspectjWeaving = EnableLoadTimeWeaving.AspectJWeaving.ENABLED)
public class EventCoreConfig {
#Bean
public LoadTimeWeaver loadTimeWeaver() {
// https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/tomcat/InstrumentableClassLoader.html
return new TomcatLoadTimeWeaver(new WebappClassLoader());
}
#Bean
#Primary
public PlatformTransactionManager txManager(DataSource dataSource) {
DataSourceTransactionManager txManager = new DataSourceTransactionManager(dataSource);
AnnotationTransactionAspect aspect = new AnnotationTransactionAspect();
aspect.setTransactionManager(txManager);
return txManager;
}
}
Here is the portion of my pom.xml that is adding dependencies of interest:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>ch.vorburger.mariaDB4j</groupId>
<artifactId>mariaDB4j</artifactId>
<version>2.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.4.0</version>
<scope>test</scope>
</dependency>
Any help will be greatly appreciated. I know this is a little advanced topic, but I do not think it should be that complicated. I think Spring's documentation lacks examples of how to properly perform this kind of configuration. Also, I haven't found any success stories over there with a similar setup.

Spring Controller - Testing to receive nested dto with multipartfile inside with postman

I'm working on a service which accepts a DTO in a POST method and creates an entity based on that DTO. Nested inside is a multipart file, which is going to be an image used by the entity that will be created.
Using postman to test my backend, I keep receiving an seemingly empty DTO. The three logs inside the controller return null, 0 and null respectively.
This is how I setup my data, which I am quite sure is the problem:
I converted my image into a base64 string, which as far as I know is the only way I can post a nested image.
Code
Controller
#PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<InventoryComponentDto> create(#ModelAttributee InventoryComponentDto request) {
System.out.println(request.getDescription());
System.out.println(request.getMinimal_supply());
System.out.println(request.getComponent());
InventoryComponentDto result = inventoryComponentService.create(request);
if (result == null) {
return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
}
return ResponseEntity.ok(result);
}
InventoryComponentDto
public class InventoryComponentDto {
private ComponentDto component;
private String description;
private Date createdAt;
private Date updatedAt;
private int minimal_supply;
private int supply;
}
ComponentDto
public class ComponentDto {
private Long id;
private int number;
private String name;
private FileDto image;
}
FileDto
public class FileDto {
private String name;
private String type;
private String url;
private MultipartFile data;
}
What would be the way for me to adequately create my dto in postman, including an image?
Update
"status": 400,
"error": "Bad Request",
"message": "JSON parse error: (was java.lang.NullPointerException); nested exception is com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: com.package.MCI.dto.InventoryComponentDto[\"component\"]->com.package.MCI.dto.ComponentDto[\"image\"])",
"trace": "org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: (was java.lang.NullPointerException); nested...
You need to create a custom jackson deserializer.
//CustomDeserializer
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.springframework.util.Base64Utils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;
public class CustomDeserializer extends StdDeserializer<FileDTO> {
public CustomDeserializer() {
super(FileDTO.class);
}
protected CustomDeserializer(Class<?> vc) {
super(vc);
}
#Override
public FileDTO deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext)
throws IOException, JsonProcessingException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
String name = node.get("name").asText();
String url = node.get("url").asText();
String type = "." + node.get("type").asText();
String fileBase64 = node.get("data").asText();
byte[] fileBytes = Base64Utils.decodeFromString(fileBase64);
FileItem fileItem = new DiskFileItem(name, "image/jpg", false, name + type,
fileBytes.length, null);
fileItem.getOutputStream().write(fileBytes);
fileItem.getOutputStream().flush();
MultipartFile file = new CommonsMultipartFile(fileItem);
fileItem.getOutputStream().close();
FileDTO fileDTO = new FileDTO();
fileDTO.setName(name);
fileDTO.setUrl(url);
fileDTO.setType(type);
fileDTO.setData(file);
return fileDTO;
}
}
And use it like:
//FileDTO
#JsonDeserialize(using = CustomDeserializer.class)
public class FileDTO {
You need these two dependencies:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>

How do I use spring data jpa to query jsonb column?

I'm having a problem getting this native query right against a postgres 9.4 instance.
My repository has a method:
#Query(value = "SELECT t.* " +
"FROM my_table t " +
"WHERE t.field_1 = ?1 " +
"AND t.field_2 = 1 " +
"AND t.field_3 IN ?2 " +
"AND t.jsonb_field #>> '{key,subkey}' = ?3",
nativeQuery = true)
List<Entity> getEntities(String field1Value,
Collection<Integer> field3Values,
String jsonbFieldValue);
But the logs show this:
SELECT t.* FROM my_table t
WHERE t.field_1 = ?1
AND t.field_2 = 1
AND t.field_3 IN ?2
AND t.jsonb_field ? '{key,subkey}' = ?3
And I get this exception:
Internal Exception: org.postgresql.util.PSQLException: No value
specified for parameter 2.
I logged the parameters directly before method invocation, and they are all supplied.
I'm not sure why #>> shows ? in the log. Do I need to escape #>>? Do I need to format the collection for IN? Do I need to escape the json path?
When I execute the query directly against the db, it works. Example:
SELECT *
FROM my_table t
WHERE t.field_1 = 'xxxx'
AND t.field_2 = 1
AND t.field_3 IN (13)
AND t.jsonb_field #>> '{key,subkey}' = 'value'
I found very helpful the Specification api from spring data.
Let's say we have an entity with name Product and a property with name title of type JSON(B).
I assume that this property contains the title of the Product in different languages. An example could be: {"EN":"Multicolor LED light", "EL":"Πολύχρωμο LED φώς"}.
The source code below finds a (or more in case it is not a unique field) product by title and locale passed as arguments.
#Repository
public interface ProductRepository extends JpaRepository<Product, Integer>, JpaSpecificationExecutor<Product> {
}
public class ProductSpecification implements Specification<Product> {
private String locale;
private String titleToSearch;
public ProductSpecification(String locale, String titleToSearch) {
this.locale = locale;
this.titleToSearch = titleToSearch;
}
#Override
public Predicate toPredicate(Root<Product> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
return builder.equal(builder.function("jsonb_extract_path_text", String.class, root.<String>get("title"), builder.literal(this.locale)), this.titleToSearch);
}
}
#Service
public class ProductService {
#Autowired
private ProductRepository productRepository;
public List<Product> findByTitle(String locale, String titleToSearch) {
ProductSpecification cs = new ProductSpecification(locale, titleToSearch);
return productRepository.find(cs);
// Or using lambda expression - without the need of ProductSpecification class.
// return productRepository.find((Root<ProductCategory> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
// return builder.equal(builder.function("jsonb_extract_path_text", String.class, root.<String>get("title"), builder.literal(locale)), titleToSearch);
// });
}
}
You can find another answer about the way you should use the Spring Data here.
Hope that helps.
If the operator is being converted to a question mark for one reason or another, then you should try using the function instead. You can find the corresponding function using \doS+ #>> in the psql console. It tells us the function called is jsonb_extract_path_text. This would make your query:
#Query(value = "SELECT t.* " +
"FROM my_table t " +
"WHERE t.field_1 = ?1 " +
"AND t.field_2 = 1 " +
"AND t.field_3 IN ?2 " +
"AND jsonb_extract_path_text(t.jsonb_field, '{key,subkey}') = ?3",
nativeQuery = true)
Maybe this is an old topic, but I'm putting here search in jsonb by field using spring specification.
If you want to search with "LIKE" you need to create like disjunction with the following code:
final Predicate likeSearch = cb.disjunction();
After that, let's assume u have jsonb field in your object which is address, and address has 5 fields. To search in all these fields you need to add "LIKE" expression for all fields:
for (String field : ADDRESS_SEARCH_FIELDS) {
likeSearch.getExpressions().add(cb.like(cb.lower(cb.function("json_extract_path_text", String.class,
root.get("address"), cb.literal(field))), %searchKey%));
}
Where cb is the same criteriaBuilder. %searchKey% is what you want to search in address fields.
Hope this helps.
You can also use the FUNC JPQL keywork for calling custom functions and not use a native query.
Something like this,
#Query(value = "SELECT t FROM my_table t "
+ "WHERE t.field_1=:field_1 AND t.field_2=1 AND t.field_3 IN :field_3 "
+ "AND FUNC('jsonb_extract_path_text', 'key', 'subkey')=:value")
List<Entity> getEntities(#Param("field_1") String field_1, #Param("field_3") Collection<Integer> field_3, #Param("value") String value);
I suggest not following this way, I prefer to follow generic CRUD way (also working on advanced auto generated DAO methods in way of StrongLoop Loopback does, for Spring Data Rest maven plugin, but it is experimental in the moment only).
But with this JSON, now what to do... I am looking for something similar to MongoDB JSON processing in Spring Data via #Document annotation, however this is not yet available. But there are other ways :-)
In general it is about implementing your JSON user type (UserType interface):
public class YourJSONBType implements UserType {
Finally you need to enhance your JPA classes with specification of your implemented user type:
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
#TypeDef(name = "JsonbType", typeClass = YourJSONBType.class)
public class Person {
#Id
#GeneratedValue
private Long id;
#Column(columnDefinition = "jsonb")
#Type(type = "JsonbType")
private Map<String,Object> info;
}
look at another related articles here:
Mapping PostgreSQL JSON column to Hibernate value type
The full implementation example is available here:
https://github.com/nzhong/spring-data-jpa-postgresql-json
https://github.com/mariusneo/postgres-json-jpa
Similar, but little different example is available here:
http://www.wisely.top/2017/06/27/spring-data-jpa-postgresql-jsonb/?d=1
Sharing my own example as I struggled decomposing the provided answers for my specific needs. Hopefully this helps others. My examples are in groovy and I'm integrating with a postgres SQL database. This is a simple example of how to search a JSON column on a field called "name" and use paging.
JSON support class
#TypeDefs([#TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)])
#MappedSuperclass
class JSONSupport {}
The entity class:
#Entity
#Table(name = "my_table")
class MyEntity extends JSONSupport {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
Long pk
#Type(type = "jsonb")
#Column(columnDefinition = "jsonb")
String jsonData
}
The specification class
class NameSpecification implements Specification<MyEntity> {
private final String name
PhoneNumberSpecification(String name) {
this.name = name
}
#Override
Predicate toPredicate(Root<ContactEntity> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
return builder.equals(
builder.function(
"jsonb_extract_path_text",
String.class,
root.<String>get("jsonData"),
builder.literal("name")
),
this.name
)
}
}
The repository
interface MyEntityRepository extends PagingAndSortingRepository<MyEntity, Long>, JpaSpecificationExecutor<MyEntity> {}
The usage
#Service
class MyEntityService {
private final MyEntityRepository repo
MyEntityService(MyEntityRepository repo) {
this.repo = repo
}
Page<MyEntity> getEntitiesByNameAndPage(String name, Integer page, Integer pageSize) {
PageRequest pageRequest = PageRequest.of(page, pageSize, Sort.by("pk"))
NameSpecification spec = new NameSpecification(name)
return repo.findAll(spec, pageRequest)
}
}
Create a table in postgres DB
CREATE TABLE shared.my_data (
id serial PRIMARY KEY,
my_config jsonb
);
Insert data into the table
INSERT into shared.my_data (id, my_config) VALUES( 1,
'{"useTime": true,
"manualUnassign": false,
"require":true,
"blockTime":10,
"additionalHours":1,
"availablegroups":[10,20,30]
}')
Check data in table:
select * from shared.tenant_data
Spring boot Java project
Java version: 11
Spring version: 2.7.1
Maven dependency on POM.xml file.
For postgres JOSNB, we need particular
vladmihalcea dependency version 2.14.0
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
JSON object class
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class MyConfig {
#JsonProperty("useTime")
private boolean useTime;
#JsonProperty("manualUnassign")
private boolean manualUnassign;
#JsonProperty("require")
private boolean require;
#JsonProperty("additionalHours")
private int additionalHours;
#JsonProperty("blockTime")
private int blockTime;
#JsonProperty("availableGroup")
private List<Integer> availableGroup;
}
[Entity]Root object to encapsulate column in table row
import com.vladmihalcea.hibernate.type.json.JsonBinaryType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import javax.persistence.*;
#Data
#Entity
#Table(name = "my_data", schema = "shared")
#Builder
#NoArgsConstructor
#AllArgsConstructor
#TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
public class MyData {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
#Type(type = "jsonb")
#Column(columnDefinition = "jsonb")
private MyConfig myConfig;
}
Repository layer
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
#Repository
public interface MyDataRepo extends JpaRepository<MyData, Long> {
}
Service layer
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
#Service
public class MyDataService {
#Autowired
private MyDataRepo myDataRepo;
public List<MyData> getAllMyspecificData(){
List<MyData> allMyData = myDataRepo.findAll();
return allMyData;
}
}
REST End point
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
#RestController
#RequestMapping(path = "/my")
public class MyResouce {
#Autowired
MyDataService myDataService;
#GetMapping("/data")
public ResponseEntity<Object> getAllMyData() {
List<MyData> myDataList =
myDataService.getAllMyspecificData();
return new ResponseEntity<>(myDataList, HttpStatus.OK);
}
}

Resources