Appending custom conditions on spring data jpa repository method queries - spring

Short Version
I am looking for a way to have all the findBy methods of a repository class appended with a particular condition
Full Version
Let's assume I have a Product entity and Customer entity. Both of them extends the OwnerAwareEntity and they inherit ownerRef field which identifies the owner of the entity( It could be a merchant or a partner ). I want to have the findBy methods of the Product and Customer modified in runtime such that they are appended with an additional condition of the ownerRef. The ownerRef value could be identified from the user session.
Example
The parent entity class that provides the common ownerRef field
public class OwnerAwareEntity implements Serializable {
private String ownerRef;
}
Customer entity extending OwnerAwareEntity
public class Customer extends OwnerAwareEntity {
private String firstname;
private String mobile ;
}
Product entity extending OwnerAwareEntity
public class Product extends OwnerAwareEntity {
private String code;
private String name;
}
Repository class for Product & Customer extending an OwnerAwareRepository
public interface OwnerAwareRepository extends JpaRepository {
}
public interface ProductRepository extends OwnerAwareRepository {
Product findByCode(String code );
}
public interface CustomerRepository extends OwnerAwareRepository {
Customer findByFirstname(String firstname );
}
This, when executed, should result in a query like below
select P from Product P where P.code=?1 and P.ownerRef='aValue'
&
select C from Customer C where C.firstname=?1 and C.ownerRef='aValue'
What should be my approach to have this appending of condition achieved?. I only want this appending to be happening when the parent repository is OwnerAwareRepository.

TL;DR: I used #Filter of Hibernate and then created an Aspect to intercept the methods
Defined a base class entity with the following structure
OwnerAwareEntity.java
import org.hibernate.annotations.Filter;
import org.hibernate.annotations.FilterDef;
import org.hibernate.annotations.ParamDef;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
import java.io.Serializable;
#MappedSuperclass
#FilterDef(name = "ownerFilter", parameters = {#ParamDef(name = "ownerRef", type = "long")})
#Filter(name = "ownerFilter", condition = "OWNER_REF = :ownerRef")
public class OwnerAwareEntity implements Serializable{
#Column(name = "OWNER_REF",nullable = true)
private Long ownerRef;
}
We set the filter on this entity. The hibernate #Filter allows us to set a condition to be appended to the select where clause.
Next, defined a base repository for the entity of type OwnerAwareEntity
OwnerAwareRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;
#NoRepositoryBean
public interface OwnerAwareRepository<T, ID extends java.io.Serializable> extends JpaRepository<T, ID> {
}
Created an Aspect that will intercept all the methods from the repositories that extend OwnerAwareRepository
OwnerFilterAdvisor.java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.hibernate.Filter;
import org.hibernate.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
#Aspect
#Component
#Slf4j
public class OwnerFilterAdvisor {
#PersistenceContext
private EntityManager entityManager;
#Pointcut("execution(public * com.xyz.app.repository.OwnerAwareRepository+.*(..))")
protected void ownerAwareRepositoryMethod(){
}
#Around(value = "ownerAwareRepositoryMethod()")
public Object enableOwnerFilter(ProceedingJoinPoint joinPoint) throws Throwable{
// Variable holding the session
Session session = null;
try {
// Get the Session from the entityManager in current persistence context
session = entityManager.unwrap(Session.class);
// Enable the filter
Filter filter = session.enableFilter("ownerFilter");
// Set the parameter from the session
filter.setParameter("ownerRef", getSessionOwnerRef());
} catch (Exception ex) {
// Log the error
log.error("Error enabling ownerFilter : Reason -" +ex.getMessage());
}
// Proceed with the joint point
Object obj = joinPoint.proceed();
// If session was available
if ( session != null ) {
// Disable the filter
session.disableFilter("ownerFilter");
}
// Return
return obj;
}
private Long getSessionOwnerRef() {
// Logic to return the ownerRef from current session
}
}
The advisor is set to intercept all the methods from classes that extends the OwnerAwareRepository. On the interception, the current hibernate Session is obtained from entityManager ( of current persistence context ) and the filter is enabled with the param value of "ownerRef".
Also have a configuration file created to have the advisor scanned
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
#Configuration
#ComponentScan(basePackages = {"com.xyz.app.advisors"})
public class AOPConfig {
}
Once these files are in place, you need to have the following things done for the entities that need to be owner aware
The entity needs to extend OwnerAwareEntity
The entity repository class need to extend OwnerAwareRepository
Dependencies
This setup requires the spring aop to be in the dependencies. You may add the following to the pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Advantages
Works with all select queries ( findBy methods, findAll etc )
#Query methods also gets intercepted
Simple implementation
Caveats
The where clause of delete or update is not affected by
this filter.
If the repository contains a save/update/delete method and if the
method is not tagged as #Transactional, then interceptor will give
error ( You can catch and have the method proceed normally in these
cases)

You can use Predicate of QueryDSL (or Specification) in Spring Data JPA methods.
Example:
interface UserRepository extends CrudRepository<User, Long>, QueryDslPredicateExecutor<User> {
}
Predicate predicate = QUser.user.firstname.equalsIgnoreCase("dave")
.and(user.lastname.startsWithIgnoreCase("mathews"));
userRepository.findAll(predicate);
To work with QueryDSL add to you pom.xml:
<dependencies>
//..
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>4.1.4</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>4.1.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Then compile your project and you will get Q-classes of your entities.
More info is here.

Related

Unable to get table autocreated in spring module integration test

I have a parameters module in my project which other modules are dependent on. I'd like to write instegration tests for this module in separate manner. This module is a spring based one and has some db related logic, though db migration scripts are not available in this module since it is something that is out of its area of resposibility. By the way in-memory H2 instance is used for testing purposes.
What I'm trying to achieve is to make spring/hibernate create DB tables based on single #Entity class present in this module, it is called ParameterEntity.
It is defined like this:
#Entity
#Table(name = "SYSTEM_PARAMETERS")
public class ParameterEntity {
#Id
#Column(name = "id")
private String id;
#Column(name = "value")
private String value;
// accessors go here...
}
In my application-test.yml file I provide the following props:
spring:
jpa:
hibernate:
ddl-auto: create
database: h2
show-sql: true
And define integration test class like this:
#ExtendWith(SpringExtension.class)
#EnableConfigurationProperties
#EntityScan("path.to.package.where.parameter.entity.resides")
#ConstextConfiguration(initializers = ConfigFileApplicationContextInitializer.class)
#ActiveProfiles("test")
#TestInstance(Lifecycle.PER_CLASS)
public class ParametersIntegrationTest {
#Autowired
private ParameterWarden warden;
#BeforeEach
public void setUp() {
warden.initialize();
}
#Configuration
#ComponentScan("base.module.package")
static class TestConfiguration {
// here comes some test beans which will be used in testing purposes only
}
}
In #BeforeEach method ParameterWarden class calls repository class which in turn make some calls to database to retrieve parameter entities from database and these calls fail because SYSTEM_PARAMETERS is missing.
Could anyone, please, let me know what am I missing here and how can I make spring or hibernate create table based on the entity present in my project. Even the place where I can debug this would be nice to know.
It seems like I need another magical thing that will trigger this functionality but I was unable to figure out what exactly I need.
Any help is really appreciated, thank you very much for your time!
p.s. I can not use #SpringBootTest annotation since this module uses only some spring features and is not a spring boot application itself. It is used as a dependecy in another spring boot applications.
Can you still use #DataJpaTest?
package com.test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
#ExtendWith(SpringExtension.class)
#DataJpaTest
#ContextConfiguration(classes = TestEntityRepository.class)
#TestPropertySource(properties = {
"spring.jpa.hibernate.ddl-auto=create",
"spring.datasource.platform=h2",
"spring.jpa.show-sql=true"
})
#EnableAutoConfiguration
public class IntegrationTest {
#Autowired
TestEntityRepository testEntityRepository;
#Test
void testCreateRead() {
var saved = testEntityRepository.save(new TestEntity("test"));
Assertions.assertNotNull(saved);
var read = testEntityRepository.findById(saved.getId());
Assertions.assertNotNull(read);
}
}
Where repository and entity in the com.test package are:
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
#Repository
public interface TestEntityRepository extends CrudRepository<TestEntity, Long> { }
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
#Entity
#Data
#NoArgsConstructor
public class TestEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column
private Long id;
#Column
private String attr;
public TestEntity(String attr) {
this.attr = attr;
}
}
Dependecies used (through dependency management and Spring Boot BOM of version 2.3.4.RELEASE):
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
Alternatively, if you want to fully decouple tests from Spring you can use org.hibernate.tool.hbm2ddl.SchemaExport in some utility logic since that's what's really executing under the hood. I use this approach since my project requires some extra steps to setup the database, however, it might be too complicated for some use cases.

Spring Boot and Couchbase Connection Error

I am starting to learn spring boot and couchbase together, and implementing a simple custom query. However when I hit localhost:8889/agents/findByAgentId/14045, I got "this site can't be reached" error. What I missed here? I will appreciate any response. Thank you
here is the Entity Class
import com.sun.istack.internal.NotNull;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.data.couchbase.core.mapping.Field;
#Document
#Setter
#Getter
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode
public class Agent {
#Id
#NotNull
#Field
private String AgentLeaderId;
#Field
private String agentPreference;
#Field
private String agency;
#Field
private String mobilePhone;
}
Here is the Configuration Class
import org.springframework.context.annotation.Configuration;
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
#Configuration
public class AgentConfig extends AbstractCouchbaseConfiguration {
#Override
public String getConnectionString() {
return ("127.0.0.1");
}
#Override
public String getUserName() {
return "******";
}
#Override
public String getPassword() {
return "*******";
}
#Override
public String getBucketName() {
return "******";
}
}
Here is the Repository
import com.bit.pruleads.entity.Agent;
import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed;
import org.springframework.data.couchbase.core.query.ViewIndexed;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.data.couchbase.repository.Query;
import org.springframework.stereotype.Repository;
import java.io.Serializable;
import java.util.List;
#Repository
#N1qlPrimaryIndexed
#ViewIndexed(designDoc = "primaryLeadsData")
public interface AgentRepository extends CouchbaseRepository<Agent, String > {
#Query("#{#n1ql.selectEntity} WHERE agentLeaderId = $1 AND #{#n1ql.filter}")
List<Agent> findByAgentId(String agentId);
#Query("#{#n1ql.selectEntity} WHERE mobilePhone = $1 AND #{#n1ql.filter}")
List<Agent> findAgentsByPhoneNumber(String phoneNumber);
}
Controller Class
import com.bit.pruleads.entity.Agent;
import com.bit.pruleads.repository.AgentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
#RestController
public class AgentController {
#Autowired
private AgentRepository agentRepository;
public AgentController(AgentRepository agentRepository) {
this.agentRepository = agentRepository;
}
#PostMapping("/findByAgentId/{id}")
public List<Agent> findByAgentId(#PathVariable String id) {
return agentRepository.findByAgentId(id);
}
#PostMapping("/findAgentsByPhoneNumber/{phoneNumber}")
public List<Agent> findAgentByPhoneNumber(#PathVariable String phoneNumber) {
return agentRepository.findAgentsByPhoneNumber(phoneNumber);
}
}
Here is the pom file
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-couchbase</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
Here is the Application properties
#Server port
server.port =8889
Here is some of the Logs
No active profile set, falling back to default profiles: default
Bootstrapping Spring Data Couchbase repositories in DEFAULT mode.
Finished Spring Data repository scanning in 456ms.
Found 0 Couchbase
repository interfaces.
Bootstrapping Spring Data Couchbase repositories in DEFAULT mode.
Finished Spring Data repository scanning in 35ms.
Found 1 Couchbase repository interfaces.
Opened bucket "leads-data"
Started PruleadsApplication in 7.502 seconds (JVM running for 8.685)
Closed bucket "leads-data"
Node disconnected
Completed shutdown and closed all open buckets
Process finished with exit code 0
I think you need to have spring-boot-starter-web included in your pom. With no web server, spring-boot starts and finds it has nothing to do so it will just stop.
First of all, use this version instead:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-couchbase</artifactId>
<version>4.1.1</version>
</dependency>
You will need to change a few imports, so check the documentation here: https://docs.spring.io/spring-data/couchbase/docs/current/reference/html/#reference
Remove the annotations #N1qlPrimaryIndexed and #ViewIndexed, they have been replacing by new annotations: https://docs.spring.io/spring-data/couchbase/docs/current/reference/html/#couchbase.repository.indexing
You don't need to create the indexes right now, just make sure that you have a primary index on your database and you should be able to get something up and running:
CREATE PRIMARY INDEX ON MY_BUCKET
Here is an example:
https://github.com/deniswsrosa/spring-data-couchbase-quickstart
Just Run the class "QuickStartCouchbase" and after the application startup, the method "run" inside "CmdRunner" should be called.

#Timed not working despite registering TimedAspect explicitly - spring boot 2.1

I need to measure method-metrics using micrometer #Timed annotation. As it doesn't work on arbitrary methods; i added the configuration of #TimedAspect explicitly in my spring config. Have referred to this post for exact config
Note: have tried adding a separate config class JUST for this, as well as including the TimedAspect bean as part of my existing configuration bean
How to measure service methods using spring boot 2 and micrometer
Yet, it unfortunately doesn't work. The Bean is registred and the invocation from config class goes thru successfully on startup. Found this while debugging. However, the code in the #Around never seems to execute.
No error is thrown; and im able to view the default 'system' metrics on the /metrics and /prometheus endpoint.
Note: This is AFTER getting the 'method' to be invoked several times by executing a business flow. I'm aware that it probably doesn't show up in the metrics if the method isn't invoked at all
Versions: spring-boot 2.1.1, spring 5.3, micrometer 1.1.4, actuator 2.1
Tried everything going by the below posts:
How to measure service methods using spring boot 2 and micrometer
https://github.com/izeye/sample-micrometer-spring-boot/tree/timed-annotation
https://github.com/micrometer-metrics/micrometer/issues/361
Update: So, the issue seems to be ONLY when the Timed is on an abstract method, which is called via another method. Was able to reproduce it via a simple example. Refer to the #Timed("say_hello_example") annotation. It simply gets ignored and doesnt show up when i hit the prometheus endpoint.
Code:
Abstract Class
public abstract class AbstractUtil {
public abstract void sayhello();
public void sayhellowithtimed(String passedVar) {
System.out.println("Passed var =>"+passedVar);
System.out.println("Calling abstract sayhello....");
sayhello();
}
}
Impl Class
#Component
#Scope("prototype")
public class ExampleUtil extends AbstractUtil {
public static final String HELLO = "HELLO";
#Timed("dirwatcher_handler")
public void handleDirectoryWatcherChange(WatchEvent event){
System.out.println("Event kind:" + event.kind() + ". File affected: " + event.context());
}
#Timed("say_hello_example")
#Override
public void sayhello() {
System.out.println(HELLO);
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
A simple DirWatcher implementation class...
package com.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Scope;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.*;
#Component
#Scope("prototype")
public class StartDirWatcher implements ApplicationListener<ApplicationStartedEvent> {
#Value("${directory.path:/apps}")
public String directoryPath;
#Autowired
private ExampleUtil util;
private void monitorDirectoryForChanges() throws IOException, InterruptedException {
WatchService watchService = FileSystems.getDefault().newWatchService();
Path path = Paths.get(directoryPath);
path.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
WatchKey key;
while ((key = watchService.take()) != null) {
for (WatchEvent<?> event : key.pollEvents()) {
util.handleDirectoryWatcherChange(event);
util.sayhellowithtimed("GOD_OF_SMALL_THINGS_onAPPEvent");
}
key.reset();
}
}
#Override
public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
try {
monitorDirectoryForChanges();
} catch (Throwable e) {
System.err.println("ERROR!! "+e.getMessage());
e.printStackTrace();
}
}
}
The Spring Boot Application Class
package com.example;
import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
#EnableAspectJAutoProxy
#ComponentScan
#Configuration
#SpringBootApplication
public class ExampleStarter{
#Bean
MeterRegistryCustomizer<PrometheusMeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags("app.name", "example.app");
}
#Bean
TimedAspect timedAspect(MeterRegistry reg) {
return new TimedAspect(reg);
}
public static void main(String[] args) {
SpringApplication.run(ExampleStarter.class, args);
}
}
The main pom.xml file
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.metrics.timed.example</groupId>
<artifactId>example-app</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
</dependencies>
I use spring boot 2.2.6.RELEASE and this MetricConfig works for me
#Configuration
public class MetricConfig {
#Bean
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags("application", "my app");
}
#Bean
TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
In application.yml
management:
endpoints:
web:
exposure:
include: ["health", "prometheus"]
endpoint:
beans:
cache:
time-to-live: 10s
#Timed use AOP(Aspect oriented programming) concept, in which proxy doesn't pass on to the second level of the method.
you can define the second level of method in new bean/class. this way #Timed will work for second level of method call.
I had the same problem, in my case I realised that the metric got visible under actuator/metrics only after the method had been called at least once.
Unlike with manually created timers/counters, where they get visible directly after startup.

spring boot multiple data sources

I was trying to follow this link to configure two databases in my application.
In the latest spring we don't have org.springframework.boot.autoconfigure.jdbc.TomcatDataSourceConfiguration class.
What is the alternative for this to use.
I am using gradle for my spring boot application
I prefer using "org.apache.tomcat.jdbc.pool.DataSource", and then configure my data sources manually.
org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
I set the fully qualified name, because you should return "javax.sql.DataSource" from whichever factory method you make.
Getting multiple data sources with spring-boot auto configuration is a pan, since the opinionated view is that you should just create a different service for each data source.
That is not always possible, so here's everything I do when I need multiple DataSources in a single application.
Disable the auto configuration like so:
#SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class YourApp{}
Create a configuration properties for your datasources:
Properties file:
the-first.datasource.url=<insert value>
the-first.datasource.username=<insert value>
the-first.datasource.pw=<insert value>
the-first.datasource.min-idle=<insert value>
the-first.datasource.max-idle=<insert value>
the-first.datasource.max-active=<insert value>
the-first.datasource.validation-query=SELECT 1
# etc ...
the-second.datasource.url=<insert value>
the-second.datasource.username=<insert value>
the-second.datasource.pw=<insert value>
the-second.datasource.min-idle=<insert value>
the-second.datasource.max-idle=<insert value>
the-second.datasource.max-active=<insert value>
the-second.datasource.validation-query=SELECT 1
Configuration class:
import lombok.*;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#Component("theFirstDataSourceProperties")
#ConfigurationProperties("the-first.datasource")
public class TheFirstDataSourceProperties{
#NonNull
private String password;
#NonNull
private String url;
#NonNull
private String username;
private int minIdle;
private int maxIdle;
private int maxActive;
#NonNull
private String driverClassName;
#NonNull
private String validationQuery;
}
Add a data source configuration class:
Be sure and mark one of them as the "#Primary" to help with injection.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* Copyright ${year}
*
* #author J. Keith Hoopes
*/
#Configuration
public class ExampleOfMultipleDataSourceConfiguration{
#Bean(name = "theFirstDataSource")
#Primary
#Autowired
public DataSource theFirstDataSource(
TheFirstDataSourceProperties theFirstDataSourceProperties){
//Fully qualified to not conflict with generic DataSource
org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
// General
dataSource.setName("theFirstDataSourceName");
dataSource.setDriverClassName(theFirstDataSourceProperties.getDriverClassName());
// etc ....
return dataSource;
}
#Bean(name = "bDataSource")
#Autowired
public DataSource theSecondDataSource(
TheSecondDataSourceProperties theSecondDataSourceProperties){
//Fully qualified to not conflict with generic DataSource
org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
// General
dataSource.setName("theSecondDataSourceName");
dataSource.setDriverClassName(theSecondDataSourceProperties.getDriverClassName());
// etc ....
return dataSource;
}
}
Inject your custom DataSources where needed using #Qualifier so you get the correct one:)
#Qualifier("theFirstDataSource")
Profit? Yes.
Oh, and here are the basic dependencies I use.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-metadata</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.jtds</groupId>
<artifactId>jtds</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>

Why do I get Detached Entity exception when upgrading Spring Boot 1.1.4 to 1.1.5

On updating Spring Boot from 1.1.4 to 1.1.5 a simple web application started generating detached entity exceptions. Specifically, a post authentication inteceptor that bumped number of visits was causing the problem.
A quick check of loaded dependencies showed that Spring Data has been updated from 1.6.1 to 1.6.2 and a further check of the change log shows a couple of issues relating to optimistic locking, version fields and JPA issues that have been fixed.
Well I am using a version field and it starts out as Null following recommendation to not set in the specification.
I have produced a very simple test scenario where I get detached entity exceptions if the version field starts as null or zero. If I create an entity with version 1 however then I do not get these exceptions.
Is this expected behaviour or is there still something amiss?
Below is the test scenario I have for this condition. In the scenario the service layer that has been annotated #Transactional. Each test case makes multiple calls to the service layer - the tests are working with detached entities as this is the scenario I am working with in the full blown application.
The test case comprises four tests:
Test 1 - versionNullCausesAnExceptionOnUpdate()
In this test the version field in the detached object is Null. This is how I would usually create the object prior to passing to the service.
This test fails with a Detached Entity exception.
I would have expected this test to pass. If there is a flaw in the test then the rest of the scenario is probably moot.
Test 2 - versionZeroCausesExceptionOnUpdate()
In this test I have set the version to value Long(0L). This is an edge case test and included because I found reference to Zero values being used for version field in the Spring Data change log.
This test fails with a Detached Entity exception.
Of interest simply because the following two tests pass leaving this as an anomaly.
Test 3 - versionOneDoesNotCausesExceptionOnUpdate()
In this test the version field is set to value Long(1L). Not something I would usually do, but considering the notes in the Spring Data change log I decided to give it a go.
This test passes.
Would not usually set the version field, but this looks like a work-around until I figure out why the first test is failing.
Test 4 - versionOneDoesNotCausesExceptionWithMultipleUpdates()
Encouraged by the result of test 3 I pushed the scenario a step further and perform multiple updates on the entity that started life with a version of Long(1L).
This test passes.
Reinforcement that this may be a useable work-around.
The entity:
package com.mvmlabs.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;
#Entity
#Table(name="user_details")
public class User {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#Version
private Long version;
#Column(nullable = false, unique = true)
private String username;
#Column(nullable = false)
private Integer numberOfVisits;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
public Integer getNumberOfVisits() {
return numberOfVisits == null ? 0 : numberOfVisits;
}
public void setNumberOfVisits(Integer numberOfVisits) {
this.numberOfVisits = numberOfVisits;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
The repository:
package com.mvmlabs.dao;
import org.springframework.data.repository.CrudRepository;
import com.mvmlabs.domain.User;
public interface UserDao extends CrudRepository<User, Long>{
}
The service interface:
package com.mvmlabs.service;
import com.mvmlabs.domain.User;
public interface UserService {
User save(User user);
User loadUser(Long id);
User registerVisit(User user);
}
The service implementation:
package com.mvmlabs.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import com.mvmlabs.dao.UserDao;
import com.mvmlabs.domain.User;
#Service
#Transactional(propagation=Propagation.REQUIRED, readOnly=false)
public class UserServiceJpaImpl implements UserService {
#Autowired
private UserDao userDao;
#Transactional(readOnly=true)
#Override
public User loadUser(Long id) {
return userDao.findOne(id);
}
#Override
public User registerVisit(User user) {
user.setNumberOfVisits(user.getNumberOfVisits() + 1);
return userDao.save(user);
}
#Override
public User save(User user) {
return userDao.save(user);
}
}
The application class:
package com.mvmlabs;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
#Configuration
#ComponentScan
#EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
The POM:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mvmlabs</groupId>
<artifactId>jpa-issue</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-jpa-issue</name>
<description>JPA Issue between spring boot 1.1.4 and 1.1.5</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.5.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>com.mvmlabs.Application</start-class>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The application properties:
spring.jpa.hibernate.ddl-auto: create
spring.jpa.hibernate.naming_strategy: org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.database: HSQL
spring.jpa.show-sql: true
spring.datasource.url=jdbc:hsqldb:file:./target/testdb
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driverClassName=org.hsqldb.jdbcDriver
The test case:
package com.mvmlabs;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.mvmlabs.domain.User;
import com.mvmlabs.service.UserService;
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
#Autowired
UserService userService;
#Test
public void versionNullCausesAnExceptionOnUpdate() throws Exception {
User user = new User();
user.setUsername("Version Null");
user.setNumberOfVisits(0);
user.setVersion(null);
user = userService.save(user);
user = userService.registerVisit(user);
Assert.assertEquals(new Integer(1), user.getNumberOfVisits());
Assert.assertEquals(new Long(1L), user.getVersion());
}
#Test
public void versionZeroCausesExceptionOnUpdate() throws Exception {
User user = new User();
user.setUsername("Version Zero");
user.setNumberOfVisits(0);
user.setVersion(0L);
user = userService.save(user);
user = userService.registerVisit(user);
Assert.assertEquals(new Integer(1), user.getNumberOfVisits());
Assert.assertEquals(new Long(1L), user.getVersion());
}
#Test
public void versionOneDoesNotCausesExceptionOnUpdate() throws Exception {
User user = new User();
user.setUsername("Version One");
user.setNumberOfVisits(0);
user.setVersion(1L);
user = userService.save(user);
user = userService.registerVisit(user);
Assert.assertEquals(new Integer(1), user.getNumberOfVisits());
Assert.assertEquals(new Long(2L), user.getVersion());
}
#Test
public void versionOneDoesNotCausesExceptionWithMultipleUpdates() throws Exception {
User user = new User();
user.setUsername("Version One Multiple");
user.setNumberOfVisits(0);
user.setVersion(1L);
user = userService.save(user);
user = userService.registerVisit(user);
user = userService.registerVisit(user);
user = userService.registerVisit(user);
Assert.assertEquals(new Integer(3), user.getNumberOfVisits());
Assert.assertEquals(new Long(4L), user.getVersion());
}
}
The first two tests fail with detached entity exception. The last two tests pass as expected.
Now change Spring Boot version to 1.1.4 and rerun, all tests pass.
Are my expectations wrong?
Edit: This code saved to GitHub at https://github.com/mmeany/spring-boot-detached-entity-issue
There is a problem with spring-data-jpa in release 1.6.2, this has been resolved in the spring-data-jpa 1.6.4-RELEASE. As soon as a Spring Boot update pulls in the new version of spring data JPA this will become a non-issue, until then override the version of spring-data-jpa in the POM.
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.6.4.RELEASE</version>
</dependency>
Adding this to the test case fixes all issues, all tests pass as expected.

Resources