Using jsonb postgres format into Spring data jdbc - spring

I have the following test table:
CREATE TABLE user (
id UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(100),
address jsonb
)
What I want to do is loading this table into an entity
public class Buddy{
#Id
private UUID id;
private String name;
private Address address;
//getter and setter
}
where the Address is something like
public class Address {
#JsonProperty("name")
public String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Is not clear to me how can I convert my json string into a java Pojo?
is possible doing this, or is out of the scope?

With Spring Data JDBC
Sample data
-- You can't create a database table named 'user' in PostgreSQL
drop table users;
CREATE TABLE users (
id VARCHAR(10) NOT NULL PRIMARY KEY,
name VARCHAR(100),
address jsonb
);
insert into users values('1', 'Jhon Doe', '{ "name": "Main St Anytown, USA"}');
Repository
#Repository
public interface BuddyRepository extends CrudRepository<Buddy, String>{
}
You can create and register a Converter
#Configuration
#ComponentScan("com.example.demo")
public class Config extends AbstractJdbcConfiguration {
#Bean
public DataSource pgDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.postgresql.Driver");
dataSource.setUrl("jdbc:postgresql://localhost:5432/db");
dataSource.setUsername("postgres");
dataSource.setPassword("postgres");
return dataSource;
}
#Bean
public JdbcCustomConversions jdbcCustomConversions() {
final List<Converter<?, ?>> converters = new ArrayList<>();
converters.add(EntityWritingConverter.INSTANCE);
converters.add(EntityReadingConverter.INSTANCE);
return new JdbcCustomConversions(converters);
}
#Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
#WritingConverter
enum EntityWritingConverter implements Converter<Address, PGobject> {
INSTANCE;
#Override
public PGobject convert(Address source) {
ObjectMapper objectMapper = new ObjectMapper();
PGobject jsonObject = new PGobject();
jsonObject.setType("json");
try {
jsonObject.setValue(objectMapper.writeValueAsString(source));
} catch (SQLException throwables) {
throwables.printStackTrace();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return jsonObject;
}
}
#ReadingConverter
enum EntityReadingConverter implements Converter<PGobject, Address> {
INSTANCE;
#Override
public Address convert(PGobject pgObject) {
ObjectMapper objectMapper = new ObjectMapper();
String source = pgObject.getValue();
try {
return objectMapper.readValue(source, Address.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
}
}
Run
#Autowired
BuddyRepository repository;
#Override
public void run(String... arg0) throws Exception {
Optional<Buddy> pojo = repository.findById("1");
System.out.println(pojo.get().id);
System.out.println(pojo.get().address.getName());
}
Results
1 Main St Anytown, USA
With JPA and Hibernate
Try this way
Maven dependency
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-utils-hibernate-55</artifactId>
<version>${hypersistence-utils.version}</version>
</dependency>
Or Gradle dependency
compile group: 'io.hypersistence', name: 'hypersistence-utils-hibernate-55', version: '3.0.1'
--
import io.hypersistence.utils.hibernate.type.json.JsonBinaryType
import org.hibernate.annotations.TypeDef
#TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
public class Buddy{
#Id
private UUID id;
private String name;
#Type(type = "jsonb")
#Column(columnDefinition = "jsonb")
private Address address;
//getter and setter
}
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Address {
#JsonProperty("name")
public String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

Related

Parameter value [multiVLANSupport] did not match expected type [java.util.List (n/a)]

I have created an entity class that has a column which uses Attribute Converter of JPA:
#Convert(converter = StringListConverter.class)
private List<String> functionSpecificationLabel;
The converter class is :
#Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {
#Override
public String convertToDatabaseColumn(List<String> list) {
return String.join(",", list);
}
#Override
public List<String> convertToEntityAttribute(String joined) {
return new ArrayList<>(Arrays.asList(joined.split(",")));
}
}
The expected values of the column in the Tables are like
functionSpecificationLabel
multiVLANSupport,telepresence,csaid
Now I need to return the rows that have multiVLANSupport,telepresence,csaid as value in functionSpecificationLabel column.
My Query in the repository for the same is :
#Query("Select pd from ProductDetailsEntity pd where pd.functionSpecificationLabel in (:labels)")
Optional<ProductDetailsEntity> findByFunctionSpecificationLabel(#Param("labels") final List<String> labels);
Now I face the issue as :
Parameter value [multiVLANSupport] did not match expected type [java.util.List (n/a)]
I am not exactly sure if this is even possible, here is how i have implemented to store list of values in an entity class using #ElementCollection You can read more about it here https://thorben-janssen.com/hibernate-tips-query-elementcollection/
A good discussion can be found here How to persist a property of type List<String> in JPA?. My suggestion is to avoid storing any values in db based on a delimiter.
Ideally, when storing such labels it is better to map them using OneToMany relationship. Also note that this will create an additional table in this case animal_labels.
Answer 1
Repository
#Repository
public interface AnimalRepository extends JpaRepository<Animal, UUID> {
List<Animal> findDistinctAnimalsByLabelsIsIn(List<String> cute);
}
Entity class
#Entity
#Table(name = "animal")
public class Animal {
#Id
#GeneratedValue
#Type(type = "uuid-char")
private UUID id;
private String name;
#ElementCollection(targetClass = String.class)
private List<String> labels;
public Animal() {
}
public Animal(String name, List<String> labels) {
this.name = name;
this.labels = labels;
}
public UUID getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getLabels() {
return labels;
}
public void setLabels(List<String> labels) {
this.labels = labels;
}
}
Test:
#ExtendWith(SpringExtension.class)
#Transactional
#SpringBootTest(classes = TestApplication.class)
class CustomConverterTest {
#Autowired
private EntityManager entityManager;
#Autowired
private AnimalRepository animalRepository;
#Test
void customLabelConverter() {
Animal puppy = new Animal("Puppy", Arrays.asList("cute", "intelligent", "spy"));
Animal meow = new Animal("Cat", Arrays.asList("cute", "intelligent"));
entityManager.persist(puppy);
entityManager.persist(meow);
List<Animal> animalWithCutelabels = animalRepository.findDistinctAnimalsByLabelsIsIn(Arrays.asList("cute"));
List<Animal> animalWithSpylabels = animalRepository.findDistinctAnimalsByLabelsIsIn(Arrays.asList("spy"));
List<Animal> animalWithCuteAndSpylabels = animalRepository.findDistinctAnimalsByLabelsIsIn(Arrays.asList("cute", "spy"));
Assertions.assertEquals(2, animalWithCutelabels.size());
Assertions.assertEquals(1, animalWithSpylabels.size());
Assertions.assertEquals(2, animalWithCuteAndSpylabels.size());
}
}
Answer 2
If you do have any choice but to only go with the comma separated values then please find answer below for this approach:
Repository(since this is a string we cannot use list like in)
#Repository
public interface AnimalRepository extends JpaRepository<Animal, UUID> {
// Also note that the query goes as string and not list
List<Animal> findAllByLabelsContaining(String labels);
}
Test:
#Test
void customLabelConverter() {
Animal puppy = new Animal("Puppy", String.join(",", Arrays.asList("cute", "intelligent", "spy")));
Animal meow = new Animal("Cat", String.join(",", Arrays.asList("cute", "intelligent")));
entityManager.persist(puppy);
entityManager.persist(meow);
List<Animal> animalWithCutelabels = animalRepository.findAllByLabelsContaining(String.join(",", Arrays.asList("cute")));
List<Animal> animalWithSpylabels = animalRepository.findAllByLabelsContaining(String.join(",", Arrays.asList("spy")));
Assertions.assertEquals(2, animalWithCutelabels.size());
Assertions.assertEquals(1, animalWithSpylabels.size());
}
Entity:
#Entity
#Table(name = "animal")
public class Animal {
#Id
#GeneratedValue
#Type(type = "uuid-char")
private UUID id;
#Column
private String name;
#Column
private String labels;
public Animal() {
}
public Animal(String name, String labels) {
this.name = name;
this.labels = labels;
}
public UUID getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getLabels() {
if (StringUtils.isEmpty(labels)) return Collections.emptyList();
return new ArrayList<>(Arrays.asList(labels.split(AnimalLabelsConverter.DELIMITER_COMMA)));
}
public void setLabels(List<String> labels) {
if (CollectionUtils.isEmpty(labels)) {
this.labels = "";
} else {
this.labels = String.join(AnimalLabelsConverter.DELIMITER_COMMA, labels);
}
}
#Converter
public static class AnimalLabelsConverter implements AttributeConverter<List<String>, String> {
private static final String DELIMITER_COMMA = ",";
#Override
public String convertToDatabaseColumn(List<String> labels) {
if (CollectionUtils.isEmpty(labels)) return "";
return String.join(DELIMITER_COMMA, labels);
}
#Override
public List<String> convertToEntityAttribute(String dbData) {
if (StringUtils.isEmpty(dbData)) return Collections.emptyList();
return new ArrayList<>(Arrays.asList(dbData.split(DELIMITER_COMMA)));
}
}
}

Why is the child collection is null in One-To-Many relationship of spring boot application?

I create a spring boot application with MySQL,JPA,Web dependencies,and manually config my database settings in .properties file of Spring boot. I passed compiling, and started application successfully, and adding one record is normal fine.
BUT, i use method 'findAll(Pageable pageable)' i got a problem, that was
Could not write JSON: failed to lazily initialize a collection of roleļ¼Œcould not initialize proxy - no Session
I got confused, i started to debug my code, finally i found that the child collection of the result is null, and it contained an error, which is
"Exception occurred: com.sun.jdi.InvocationException occurred invoking method.."
I tried a lot to fix my code, but no use.
who can help me?
The entity relationship is a simple one to many:
TeacherInfo entity and ClassInfo entity, teacher manage multiple classes, just simple as this.
here is the enter point of my app:
#SpringBootApplication(exclude= {
DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class,
DataSourceAutoConfiguration.class
})
#EnableTransactionManagement
public class OrmTestApplication {
public static void main(String[] args) {
SpringApplication.run(OrmTestApplication.class, args);
}
}
Database properties setting is here:
spring.datasource.primary.url=jdbc:mysql://localhost:3306/ormtest?useSSL=false
spring.datasource.primary.username=root
spring.datasource.primary.password=BlaNok2700
spring.datasource.primary.driver-class-name = com.mysql.jdbc.Driver
hibernate.hbm2ddl.auto = update
hibernate.show-sql = true
My Data base configure java code is here:
Configuration
#EnableJpaRepositories(basePackages = "com.lanjian.ormtest.repositories", entityManagerFactoryRef = "primaryEntityManagerFactory", transactionManagerRef = "primaryTransactionManager")
public class PrimaryDbConfig {
#Autowired
private Environment env;
#Bean
#ConfigurationProperties(prefix="spring.datasource.primary")
public DataSourceProperties primaryDataSourceProperties() {
return new DataSourceProperties();
}
#Bean
public DataSource primaryDataSource() {
DataSourceProperties dbProperty = primaryDataSourceProperties();
return DataSourceBuilder.create()
.driverClassName(dbProperty.getDriverClassName())
.url(dbProperty.getUrl())
.username(dbProperty.getUsername())
.password(dbProperty.getPassword())
.build();
}
#Bean
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory() {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setDataSource(primaryDataSource());
factory.setPackagesToScan("com.lanjian.ormtest.entities");
factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
Properties jpaProperties = new Properties();
jpaProperties.put("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
jpaProperties.put("hibernate.show-sql", env.getProperty("hibernate.show-sql"));
factory.setJpaProperties(jpaProperties);
return factory;
}
#Bean
public PlatformTransactionManager primaryTransactionManager() {
EntityManagerFactory factory = primaryEntityManagerFactory().getObject();
return new JpaTransactionManager(factory);
}
}
My REST controller method is here:
#Autowired
private TeacherRepository teacherRepository;
#GetMapping("/page")
public Page<TeacherInfo> page(Pageable pageable){
Page<TeacherInfo> list = teacherRepository.findAll(pageable);
return list;
}
What happened
After i started my application, and use postman send request, i got this:
got a 500 error
And i debugger my code, found this:
child collection is null
In the picture, 'classes' is a list collection, but it is null, i don't understand.
Here are the TeacherInfo entity I defined
#Entity
#Table(name = "teacher")
public class TeacherInfo {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private byte age;
private boolean male;
#OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY, mappedBy="chargedTeacher")
private List<ClassInfo> classes = new ArrayList<>();
public void initialize() {
for (ClassInfo classInfo : classes) {
classInfo.setChargedTeacher(this);
for (StudentInfo studentInfo : classInfo.getStudents()) {
studentInfo.setClassInfo(classInfo);
}
}
}
//Setters and Getters}
Here is the ClassInfo Entity i defined
#Entity
#Table(name = "class_info")
public class ClassInfo {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int capacity;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "teacher_id",nullable=false)
#JsonIgnore
private TeacherInfo chargedTeacher;
#OneToMany(cascade = CascadeType.ALL,fetch=FetchType.LAZY,mappedBy="classInfo")
private List<StudentInfo> students = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getCapacity() {
return capacity;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
public TeacherInfo getChargedTeacher() {
return chargedTeacher;
}
public void setChargedTeacher(TeacherInfo chargedTeacher) {
this.chargedTeacher = chargedTeacher;
}
public List<StudentInfo> getStudents() {
return students;
}
public void setStudents(List<StudentInfo> students) {
this.students = students;
}
}
I think that the problem may come from Transactionality and JPA Fetching types.
Your repository method is being invoked not using a transaction, which implies that the transaction is on the boundaries of the method invocation (which might not be wrong). Spring returns a Page with objects but when it tries to serialize them, transaction is gone so no way to access childs.
I would suggest to put the JPA relationship as EAGER fetching, allowing all the objects to be present on the repository result when the transaction ends.
EDIT:
Answer to comments
#Bean
public PlatformTransactionManager primaryTransactionManager(EntityManagerFactory factory) {
return new JpaTransactionManager(factory);
}

#Enumerated(EnumType.STRING) generate int column instead of varchar

I'm using spring jpa with hibernate and ms Sqlserver. I've an entity with an enum property with an #Enumerated(EnumType.STRING) annotation. When database schema is generated, the column filled with enum value has an int type instead of a char type.
Bean methods to set database properties:
private Properties getHibernateProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.dialect","org.hibernate.dialect.SQLServer2012Dialect");
properties.setProperty("hibernate.hbm2ddl.auto","create-drop");
properties.setProperty("ejb.naming_strategy","layer.controller.configuration.PhysicalNamingStrategyImpl");
properties.setProperty("hibernate.search.default.directory_provider","filesystem");
return properties;
}
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setShowSql(Boolean.TRUE);
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setDataSource(dataSource());
factory.setPackagesToScan("layer.domain","layer.application.security.authentication.entities");
factory.setJpaProperties(getHibernateProperties());
factory.afterPropertiesSet();
return factory;
}
enum and entity:
public enum Elenco {
A,
B,
C,
D;
}
#Entity
#Table(name="elenco")
public class ElencoClass extends BaseEntity<Long> {
#Column(name="nomeECognome")
private String nome;
#Enumerated(EnumType.STRING)
#Column(columnDefinition = "varchar")
private Elenco ele;
#Id
#Column(name="id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Override
protected Long getId() {
return id;
}
#Override
protected void setId(Long id) {
super.id=id;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public Elenco getEle() {
return ele;
}
public void setEle(Elenco ele) {
this.ele = ele;
}
}
This is the table generated by spring:
CREATE TABLE [dbo].[elenco](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[ele] [int] NULL,
[nome] [varchar](255) NULL,
If you can write #Enumerated(EnumType.STRING) on getter method, that will works.
Have you tried this? You still need to have #Column annotation that specify the column name but not column definition in the get method.
#Enumerated(EnumType.STRING)
#Column(name="ele")
public Elenco getEle() {...}
Use this:
#Column(name = "ele", nullable = false, columnDefinition = "enum('ELE1', 'ELE2')")

LazyInitializationException Spring and Hibernate

I am getting this exception nested exception is org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.ibm.ro.model.Location.subLocations, could not initialize proxy - no Session.
I do get that upon accessing the collection, the transaction has already been closed that's why the code is throwing this exception. Here is my sample code
#Entity
#Table(name="location")
public class Location extends BaseEntity {
private static final long serialVersionUID = 1L;
private String name;
private List<Location> subLocations;
private Location location;
#Column(name="name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#OneToMany(fetch = FetchType.LAZY, mappedBy = "location")
public List<Location> getSubLocations() {
return subLocations;
}
public void setSubLocations(List<Location> subLocations) {
this.subLocations = subLocations;
}
#ManyToOne(fetch = FetchType.LAZY)
public Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
#Override
public String toString() {
return "Location [name=" + name + ", subLocations=" + subLocations
+ "]";
}
}
Here is my DAO:
#Repository("locationDao")
public class LocationDao implements ILocationDao{
#Autowired
private SessionFactory sessionFactory;
#Override
public List<Location> getAll() {
Session sess = getSession();
return sess.createCriteria(Location.class).setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY).list();
}
}
Then here is my service :
#Service("locationService")
#Transactional
public class LocationService implements ILocationService{
#Autowired
ILocationDao dao;
#Override
public List<Location> getAll() {
return dao.getAll();
}
}
Here is the controller where the exception is being thrown:
#Controller
public class BaseController {
#Autowired
ILocationService service;
private static final String VIEW_INDEX = "index";
private final static org.slf4j.Logger logger = LoggerFactory.getLogger(BaseController.class);
#RequestMapping(value = "/", method = RequestMethod.GET)
public String location(ModelMap model) {
logger.debug(service.getAll().toString());
return VIEW_INDEX;
}
}
What can I do to fix the problem without using OpenSessionInViewFilter?
You can iterate your Location inside your service (where you still have your transaction) and call Hibernate.initialize on the elements, the force initialization of a persistent collection.
#Override
public List<Location> getAll() {
List<Location> locations = dao.getAll();
for (Location location : locations ) {
Hibernate.intialize(location.getSubLocations())
}
return locations;
}

ElementCollection and "failed to lazily initialize a collection of role" exception

I'm very new to Spring and I'm trying to figure out how to use #ElementCollection.
I have the following classes:
#Embeddable
public class Phone {
private String type;
private String areaCode;
#Column(name="P_NUMBER")
private String number;
public Phone() {
}
public Phone(String type, String areaCode, String number) {
super();
this.type = type;
this.areaCode = areaCode;
this.number = number;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public String getAreaCode() {
return areaCode;
}
public void setAreaCode(String areaCode) {
this.areaCode = areaCode;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
#Entity
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "EMP_ID")
private long id;
#ElementCollection//(fetch=FetchType.EAGER)
#CollectionTable(name = "PHONE", joinColumns = #JoinColumn(name = "OWNER_ID"))
private List<Phone> phones = new ArrayList<Phone>();;
public Employee() {
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public List<Phone> getPhones() {
return phones;
}
public void setPhones(List<Phone> phones) {
this.phones = phones;
}
}
Repository:
#Repository
public interface EmployeeRepository extends CrudRepository<Employee, Long>{
public Employee findById(long id);
}
Then I use it in main method:
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
EmployeeRepository repository = context.getBean(EmployeeRepository.class);
Phone phone = new Phone("work", "613", "494-1234");
Employee emp = new Employee();
emp.getPhones().add(phone);
repository.save(emp);
emp = repository.findById(1);
for (Phone p : emp.getPhones()) {
System.out.println(p.getNumber());
}
context.close();
}
It throws exception (when emp.getPhones() is called): Exception in thread "main" org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: elcol.repository.Employee.phones, could not initialize proxy - no Session
If I add (fetch=FetchType.EAGER) to #ElementCollection annotation(commented in the code above in Employee class) - everything is ok.
How can I fix this without FetchType.EAGER?
In findById(long id) implementation, add this Hibernate.initialize(emp.getPhones()).
Your repository service should return all the data you will need already initialized so the client that calls the service stays independent of it. In short, If you don't need employees phones on the client side, don't initialize it. If you do need it - initialize it.
EDIT
With spring data you obviously don't have the implementation, so you can specify the query which will be used, and fetch the data in the query (the question is tagged with jpa so I guess you can use JpaRepository)
#Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long>{
#Query("SELECT e FROM Employee e JOIN FETCH e.phones WHERE e.id = (:id)")
public Employee findById(long id);
}

Resources