Monday, August 27, 2018

Spring Data with Thymeleaf

Intro

Today I'll talk about more specific issues. No design patterns or algorithms this time :-). We don't always design software components from scratch. Often we have to try to make existing software components work together. 
Spring Boot is one the best free software in the Java world. It resolved a lot of configuration issues with Spring. It is very flexible and offers great functionality. 
Spring Data is part of the Spring collection of projects. It offers advanced tools for working with databases. Among the most useful is the automatic repository. A class can implement JpaRepository and most methods for working with data will be created automatically. 
Thymeleaf is a HTML template engine. It can use some of Spring Boot's features, like call methods of Spring beans in the template and a lot of other stuff. The official documentation has great tutorials. 
I used spring-boot-starter-parent versions 2.0.1.RELEASE - 2.0.4.RELEASE. Other dependencies were provided by Spring Boot.

Problem Description

The main idea of any application that is working with Spring Boot, Spring Data and Thymeleaf is to edit data in the database. spring-boot-starter-data-jpa includes Hibernate which can be used to manipulate the data in the database. Thymeleaf can be used to show the data to the user. Spring Boot wires it all together. 
A very simple scenario includes one entity with a one-to-many relationship with another entity. The user wants to be able to create a new entity and select the other entity in a HTML select box. 
Here is where the first issue shows up. With the standard Thymeleaf structure the backing bean cannot be assembled.The object that was selected in the select box with the following construct:

<form action="#" th:action="@{/<some Action>}" th:object="${beanObj}" method="post">

    .... <other fields>

    <select th:field="*{room}" class="textinput">
        <option th:each="currRoom : ${allRooms}"      
            th:value="${currRoom}" th:text="${currRoom.name}">no   
            name</option>
    </select>
</form>

is not created by Thymeleaf. I didn't find any mention of this in the official documentation.

Solution

After some debugging I found the root cause. It turned out Thymeleaf passes all the fields as parameters to the POST request. It uses the toString method to transform the object to String and add as a parameter to the POST request. It sends a parameter like this:
room: Room+[id=273,+name=room111]

In the controller method this value must be transformed back to the object form. Spring Boot uses converters to do this. 
The solution is - register the appropriate converters with the conversionService. And use these converters in the toString method of the entities to make sure the same method is used to convert to the String form and back. 

Next Problems

Sounds funny isn't it? The solution has been found but more problems? Actually the described solution works well without Spring Data. With Spring Data the conversion fails again. And Spring Boot wants you to create the entityManagerFactory bean even though this bean was not needed without Spring Data. 

Next Solutions

The problem with the entityManagerFactory bean can be resolved by means of some intensive search on the Internet. Here is the solution I ended up with:

    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource ds) {
       LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
       em.setDataSource(ds);
       em.setPackagesToScan("<some packages>");

       JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
       em.setJpaVendorAdapter(vendorAdapter);
       em.setJpaProperties(additionalProperties());
       return em;
    }

    @Bean
    public SessionFactory sessionFactory(@Qualifier("entityManagerFactory") EntityManagerFactory emf) {
        return emf.unwrap(SessionFactory.class);
    }
private Properties additionalProperties() {
        Properties properties = new Properties();
        properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
        properties.setProperty("hibernate.default_schema", "public");
        properties.setProperty("hibernate.show_sql", "true");
        // Validation will fail because the tables use bigint as the ID but it is mapped to the Integer type by Hibernate
        // Validation expects a 8-bit number as the mapping to bigint.
        properties.setProperty("hibernate.hbm2ddl.auto", "none");
        return properties;
    }

The second problem turned out to be more complicated and required a lot of debugging. Eventually I found out that spring-data somehow changes the conversion service that Spring Boot is using. Instead of the default conversionService with Spring Data mvcConversionService is used. The formatters/converters must be added in your WebMvcConfigurer class (the class that implements WebMvcConfigurer). The method is addFormatters:

    @Override
    public void addFormatters(FormatterRegistry registry) {

        registry.addConverter(new <SomeConverter>);
        ...

Now with all problems resolved Spring Data can work with Thymeleaf. 
Happy coding and diligent debugging! 

Here is a link to Javacodegeeks Spring Data Tutorials

No comments:

Post a Comment