Hi I have mongoDb database which contains separate db for different shops, but the collections inside all the dbs have same structure, when I get the request from post service I want to insert the data to the respective database based in the id in the request. Please advice how to do this in springboot java or Kotlin
AMAZON
- ProductDetails
FLIPKART
- ProductDetails
EBAY
- ProductDetails
Now I have a single database and insert all product details in a single database and I want to add different databases for different shops
spring.data.mongodb.host=mongo
spring.data.mongodb.port=27017
spring.data.mongodb.authentication-database=admin
spring.data.mongodb.username=admin
spring.data.mongodb.password=pass
spring.data.mongodb.database=admin
Since you are new to Spring boot and MongoDB, I am providing you the detailed steps as follows to connect multiple mongo DB in single application. This is one of the most simple ways to configure and connect multiple mongo DB. Hopefully, it will be helpful (don't forget to vote up if it is :-)) -
1) Package Structure -
2) Create an abstract MongoDB Config class -
package com.akash.mongo.multidb.config;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
/**
* Abstract class for configuring different MongoTemplate for different DB
* #author Akash
*
*/
public abstract class AbstractMongoDbConfig {
private String host;
private String username;
private String password;
private String database;
private int port;
public void setHost(String host) {
this.host = host;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setDatabase(String database) {
this.database = database;
}
public void setPort(int port) {
this.port = port;
}
public MongoDbFactory mongoDbFactory() {
MongoCredential mongoCredential = MongoCredential.createCredential(username, database, password.toCharArray());
MongoClient mongoClient = new MongoClient(new ServerAddress(host, port), mongoCredential, new MongoClientOptions.Builder().build());
return new SimpleMongoDbFactory(mongoClient, database);
}
public abstract MongoTemplate getMongoTemplate() throws Exception;
}
3) Extend the abstract class to create configuration for each DB
AmazonDbConfig
package com.akash.mongo.multidb.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
/**
* Configuration class for Amazon DB
* #author Akash
*
*/
#Configuration
#ConfigurationProperties(prefix="amazon.mongodb")
#EnableMongoRepositories(basePackages= {"com.akash.mongo.multidb.repository.amazon"}, mongoTemplateRef="amazonMongoTemplate")
public class AmazonDbConfig extends AbstractMongoDbConfig {
private static final Logger logger = LoggerFactory.getLogger(AmazonDbConfig.class);
#Override
#Bean(name="amazonMongoTemplate")
public MongoTemplate getMongoTemplate() throws Exception {
logger.info("Creating MongoTemplate for Amazon DB");
return new MongoTemplate(mongoDbFactory());
}
}
EbayDbConfig
package com.akash.mongo.multidb.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
/**
* Configuration class for ebay DB
* #author Akash
*
*/
#Configuration
#ConfigurationProperties(prefix="ebay.mongodb")
#EnableMongoRepositories(basePackages= {"com.akash.mongo.multidb.repository.ebay"}, mongoTemplateRef="ebayMongoTemplate")
public class EbayDbConfig extends AbstractMongoDbConfig {
private static final Logger logger = LoggerFactory.getLogger(EbayDbConfig.class);
#Override
#Bean(name="ebayMongoTemplate")
public MongoTemplate getMongoTemplate() throws Exception {
logger.info("Creating MongoTemplate for Ebay DB");
return new MongoTemplate(mongoDbFactory());
}
}
FlipkartDbConfig
package com.akash.mongo.multidb.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
/**
* Configuration class for Flipkart DB
* #author Akash
*
*/
#Configuration
#ConfigurationProperties(prefix="flipkart.mongodb")
#EnableMongoRepositories(basePackages= {"com.akash.mongo.multidb.repository.flipkart"}, mongoTemplateRef="flipkartMongoTemplate")
public class FlipkartDbConfig extends AbstractMongoDbConfig {
private static final Logger logger = LoggerFactory.getLogger(FlipkartDbConfig.class);
#Override
#Primary
#Bean(name="flipkartMongoTemplate")
public MongoTemplate getMongoTemplate() throws Exception {
logger.info("Creating MongoTemplate for Flipkart DB");
return new MongoTemplate(mongoDbFactory());
}
}
Notice that each of these configuration class is creating its own MongoTemplate and it is enabling its own MongoRepository. Also one of these needs to be #Primary otherwise the spring boot will throw error. It doesn't matter which of these is primary; ultimately these will be connecting to their own repository
4) Create entities and a repository for each DB.
You need to create a repository for each DB now. Given that your collection is same for all the DBs, I have created following sample entity -
package com.akash.mongo.multidb.entity;
import java.io.Serializable;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
/**
* Sample Entity class
* #author Akash
*
*/
#Document(collection="productDetails")
public class ProductDetails implements Serializable {
private static final long serialVersionUID = 1L;
#Id
private ObjectId id;
#Field("productName")
private String productName;
#Field("productDesc")
private String productDesc;
#Field("productQuantity")
private String productQuantity;
public ObjectId getId() {
return id;
}
public void setId(ObjectId id) {
this.id = id;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getProductDesc() {
return productDesc;
}
public void setProductDesc(String productDesc) {
this.productDesc = productDesc;
}
public String getProductQuantity() {
return productQuantity;
}
public void setProductQuantity(String productQuantity) {
this.productQuantity = productQuantity;
}
}
You can create/modify the entity class as per your collection details.
AmazonRepository
package com.akash.mongo.multidb.repository.amazon;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import com.akash.mongo.multidb.entity.ProductDetails;
/**
*
* #author Akash
*
*/
#Repository
public interface AmazonRepository extends MongoRepository<ProductDetails, ObjectId> {
}
FlipkartRepository
package com.akash.mongo.multidb.repository.flipkart;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import com.akash.mongo.multidb.entity.ProductDetails;
#Repository
public interface FlipkartRepository extends MongoRepository<ProductDetails, ObjectId> {
}
EbayRepository
package com.akash.mongo.multidb.repository.ebay;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import com.akash.mongo.multidb.entity.ProductDetails;
/**
*
* #author Akash
*
*/
#Repository
public interface EbayRepository extends MongoRepository<ProductDetails, ObjectId> {
}
Again, each repository needs to be its own package otherwise there will be errors while running the application. This is the one disadvantage of this solution where you have to create as many repository packages as no of DBs you want to connect.
5) Service implementation and connecting to different repositories
ProductDetailsService Interface
package com.akash.mongo.multidb.service;
import com.akash.mongo.multidb.entity.ProductDetails;
/**
* Sample interface with one add method
* #author Akash
*
*/
public interface ProductDetailsService {
/**
*
* #param productOrigin - the shop name i.e. Amazon, Flipkart or ebay.
* #param productDetails - the product details to add
*/
public void addProductDetails(String productOrigin, ProductDetails productDetails) throws RuntimeException;
}
ProductDetailsServiceImpl Class -
package com.akash.mongo.multidb.service;
import java.util.Map;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.repository.MongoRepository;
import com.akash.mongo.multidb.entity.ProductDetails;
/**
* Implementation of ProductDetailsService interface
* #author Akash
*
*/
public class ProductDetailsServiceImpl implements ProductDetailsService {
private static final Logger logger = LoggerFactory.getLogger(ProductDetailsServiceImpl.class);
/*
* Spring boot will autowire all the repositories along with their name
* amazonRepository - amazon repository instance
* ebayRepository - ebay repository instance and so on
*/
#Autowired
Map<String, MongoRepository<ProductDetails, ObjectId>> repositories;
#Override
public void addProductDetails(String productOrigin, ProductDetails productDetails) throws RuntimeException {
logger.info("Adding product details into {} db", productOrigin);
//if productOrigin is Amazon; repositoryName will be amazonRepository which is already present in spring boot
String repositoryName = productOrigin.toLowerCase()+"Repository";
if(repositories.containsKey(repositoryName)) {
repositories.get(repositoryName).save(productDetails);
} else {
logger.error("{} shop is undefined in DB. Check and try again", productOrigin);
throw new RuntimeException("Shop doesnot exist in MongoDb");
}
}
}
ProductOrigin you can derive from your request or headers whatever information is available to you.
6) Lastly, application.properties
Change the database, username and password details for each DB. Try not to use Admin credentials; Instead create username & password for each DB separately and update application.properties.
#MongoDb connection properties for Flipkart DB
flipkart.mongodb.database=flipkart
flipkart.mongodb.host=http://127.0.0.1
flipkart.mongodb.port=27017
flipkart.mongodb.username=flipkart
flipkart.mongodb.password=flipkart
#MongoDb connection properties for Amazon DB
amazon.mongodb.database=amazon
amazon.mongodb.host=http://127.0.0.1
amazon.mongodb.port=27017
amazon.mongodb.username=amazon
amazon.mongodb.password=amazon
#MongoDb connection properties for ebay DB
ebay.mongodb.database=ebay
ebay.mongodb.host=http://127.0.0.1
ebay.mongodb.port=27017
ebay.mongodb.username=ebay
ebay.mongodb.password=ebay
Now, if you need to add any new database, you just need to add one config class similar to AmazonDbConfig, one more package with the required repositories for that DB and connection details in application.properties. No change is required in service till your collection is same for all the DBs.
If you have multiple collections, you can add entity and repository for each collection (group all the respositories for single shop in one package) and solution should still hold good.
When defining an EntityManager in a Spring Java Config class, I can add the base packages to scan for Entity classes by calling a method on the corresponding builder:
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder) {
// Some other configuration here
builder.packages("org.foo.bar", "org.foo.baz");
return builder.build();
}
I need something similar for the places where Spring looks for Repository Interfaces. The usual way is using the #EnableJpaRepositories annotation:
#EnableJpaRepositories(basePackages = {"org.foo.barbaz"})
But I would like to have a dynamical way for defining these packages similar to the way above for the Entity locations. Something like this:
public SomeJpaRepositoryFactoryBean entityManagerFactory(JpaRepositoryFactoryBuilder builder) {
// Some other configuration here
builder.packages("org.foo.barbaz");
return builder.build();
}
Is there a way to do this with the current Spring Data JPA release is it simply not meant to be done this way?
You can use #AutoConfigurationPackage annotation to add your child module's package to scan-packages.
Remove all #EnableJpaRepositories from your child module
Add #AutoConfigurationPackage class to the top directory of your child module (similar to #SpringBootApplication, you must put this class to the top-most directory to scan all subpackages):
#AutoConfigurationPackage
public class ChildConfiguration {
}
Create spring.factories file under /resources/META-INF/spring.factories of your child module and add the configuration class:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.child.package.ChildConfiguration
Now you can #Autowired your repository from your core project. (Tested and worked)
Without Spring Boot (plain Spring MVC setup)
#EnableJpaRepositories can be used on more than one #Configuration class. That said, every module can declare its own repositories by having an own configuration class:
#Configuration
#EnableJpaRepositories(basePackages = "package1")
public class ConfigClass1 { /* ... */ }
#Configuration
#EnableJpaRepositories(basePackages = "package2")
public class ConfigClass2 { /* ... */ }
Spring Data JPA then counts in all of them (package1 and package2).
Although this is still not a programmatical way, it solves my problem.
This problem also puzzled me for almost a week.I debug the "spring application context refresh" code and org.springframework.data.jpa.repository.config.JpaRepositoriesRegistrar line by line and then I solve the problem.
I customize my EnableJpaRepository annotation and JpaRepositoriesRegistrar,then I can do anything in AbdJpaRepositoriesRegistrar("abd" is my customized classes's prefix).
AbdEnableJpaRepositories.java
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.repository.config.DefaultRepositoryBaseClass;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManagerFactory;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* basePackages
* 复制EnableJpaRepositories,Import自定义的AbdJpaRepositoriesRegistrar.
* Copy from EnableJpaRepositories,Import customized AbdJpaRepositoriesRegistrar.
*
* #author Oliver Gierke
* #author Thomas Darimont
* #author ghj
*/
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Inherited
#Import(AbdJpaRepositoriesRegistrar.class)
public #interface AbdEnableJpaRepositories {
/**
* Alias for the {#link #basePackages()} attribute. Allows for more concise annotation declarations e.g.:
* {#code #EnableJpaRepositories("org.my.pkg")} instead of {#code #EnableJpaRepositories(basePackages="org.my.pkg")}.
*/
String value() default "";
/**
* Base packages to scan for annotated components. {#link #value()} is an alias for (and mutually exclusive with) this
* attribute. Use {#link #basePackageClasses()} for a type-safe alternative to String-based package names.
*/
String basePackages() default "";
/**
* Type-safe alternative to {#link #basePackages()} for specifying the packages to scan for annotated components. The
* package of each class specified will be scanned. Consider creating a special no-op marker class or interface in
* each package that serves no purpose other than being referenced by this attribute.
*/
Class<?>[] basePackageClasses() default {};
/**
* Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
* everything in {#link #basePackages()} to everything in the base packages that matches the given filter or filters.
*/
Filter[] includeFilters() default {};
/**
* Specifies which types are not eligible for component scanning.
*/
Filter[] excludeFilters() default {};
/**
* Returns the postfix to be used when looking up custom repository implementations. Defaults to {#literal Impl}. So
* for a repository named {#code PersonRepository} the corresponding implementation class will be looked up scanning
* for {#code PersonRepositoryImpl}.
*
* #return
*/
String repositoryImplementationPostfix() default "Impl";
/**
* Configures the location of where to find the Spring Data named queries properties file. Will default to
* {#code META-INF/jpa-named-queries.properties}.
*
* #return
*/
String namedQueriesLocation() default "";
/**
* Returns the key of the {#link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to
* {#link Key#CREATE_IF_NOT_FOUND}.
*
* #return
*/
Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
/**
* Returns the {#link FactoryBean} class to be used for each repository instance. Defaults to
* {#link JpaRepositoryFactoryBean}.
*
* #return
*/
Class<?> repositoryFactoryBeanClass() default JpaRepositoryFactoryBean.class;
/**
* Configure the repository base class to be used to create repository proxies for this particular configuration.
*
* #return
* #since 1.9
*/
Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;
// JPA specific configuration
/**
* Configures the name of the {#link EntityManagerFactory} bean definition to be used to create repositories
* discovered through this annotation. Defaults to {#code entityManagerFactory}.
*
* #return
*/
String entityManagerFactoryRef() default "entityManagerFactory";
/**
* Configures the name of the {#link PlatformTransactionManager} bean definition to be used to create repositories
* discovered through this annotation. Defaults to {#code transactionManager}.
*
* #return
*/
String transactionManagerRef() default "transactionManager";
/**
* Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
* repositories infrastructure.
*/
boolean considerNestedRepositories() default false;
/**
* Configures whether to enable default transactions for Spring Data JPA repositories. Defaults to {#literal true}. If
* disabled, repositories must be used behind a facade that's configuring transactions (e.g. using Spring's annotation
* driven transaction facilities) or repository methods have to be used to demarcate transactions.
*
* #return whether to enable default transactions, defaults to {#literal true}.
*/
boolean enableDefaultTransactions() default true;
}
AbdJpaRepositoriesRegistrar.java
import org.springframework.data.jpa.repository.config.JpaRepositoryConfigExtension;
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
import java.lang.annotation.Annotation;
class AbdJpaRepositoriesRegistrar extends AbdRepositoryBeanDefinitionRegistrarSupport {
/*
* (non-Javadoc)
* #see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getAnnotation()
*/
#Override
protected Class<? extends Annotation> getAnnotation() {
return AbdEnableJpaRepositories.class;
}
/*
* (non-Javadoc)
* #see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension()
*/
#Override
protected RepositoryConfigurationExtension getExtension() {
return new JpaRepositoryConfigExtension();
}
}
AbdRepositoryBeanDefinitionRegistrarSupport.java
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport;
import org.springframework.data.repository.config.RepositoryConfigurationDelegate;
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
import org.springframework.data.repository.config.RepositoryConfigurationUtils;
import org.springframework.util.Assert;
/**
*
* #author ghj
*/
abstract class AbdRepositoryBeanDefinitionRegistrarSupport extends RepositoryBeanDefinitionRegistrarSupport implements ImportBeanDefinitionRegistrar,
ResourceLoaderAware, EnvironmentAware {
private ResourceLoader resourceLoader;
private Environment environment;
#Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
#Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
#Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
Assert.notNull(resourceLoader, "ResourceLoader must not be null!");
Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null!");
Assert.notNull(registry, "BeanDefinitionRegistry must not be null!");
// Guard against calls for sub-classes
if (annotationMetadata.getAnnotationAttributes(getAnnotation().getName()) == null) {
return;
}
// 使用自定义的AbdAnnotationRepositoryConfigurationSource
AbdAnnotationRepositoryConfigurationSource configurationSource = new AbdAnnotationRepositoryConfigurationSource(
annotationMetadata, getAnnotation(), resourceLoader, environment);
RepositoryConfigurationExtension extension = getExtension();
RepositoryConfigurationUtils.exposeRegistration(extension, registry, configurationSource);
RepositoryConfigurationDelegate delegate = new RepositoryConfigurationDelegate(configurationSource, resourceLoader,
environment);
delegate.registerRepositoriesIn(registry, extension);
}
}
AbdAnnotationRepositoryConfigurationSource.java. You can override getBasePackages then you can return any packages you want.
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
*
* #author ghj
*/
class AbdAnnotationRepositoryConfigurationSource extends AnnotationRepositoryConfigurationSource {
private static final String BASE_PACKAGES = "basePackages";
private static final String BASE_PACKAGE_CLASSES = "basePackageClasses";
private final AnnotationMetadata configMetadata;
private final AnnotationAttributes attributes;
private final Environment environment;
AbdAnnotationRepositoryConfigurationSource(AnnotationMetadata metadata, Class<? extends Annotation> annotation, ResourceLoader resourceLoader, Environment environment) {
super(metadata, annotation, resourceLoader, environment);
this.attributes = new AnnotationAttributes(metadata.getAnnotationAttributes(annotation.getName()));
this.configMetadata = metadata;
this.environment = environment;
}
#Override
public Iterable<String> getBasePackages() {
String value = attributes.getStringArray("value")[0];
String basePackages = attributes.getStringArray(BASE_PACKAGES)[0];
Class<?>[] basePackageClasses = attributes.getClassArray(BASE_PACKAGE_CLASSES);
// Default configuration - return package of annotated class
if (StringUtils.isEmpty(value) && StringUtils.isEmpty(basePackages) && basePackageClasses.length == 0) {
String className = configMetadata.getClassName();
return Collections.singleton(ClassUtils.getPackageName(className));
}
String[] packagesFromValue = parsePackagesSpel(value);
String[] packagesFromBasePackages = parsePackagesSpel(basePackages);
Set<String> packages = new HashSet<>();
packages.addAll(Arrays.asList(packagesFromValue));
packages.addAll(Arrays.asList(packagesFromBasePackages));
for (Class<?> typeName : basePackageClasses) {
packages.add(ClassUtils.getPackageName(typeName));
}
return packages;
}
private String[] parsePackagesSpel(String raw) {
if (!raw.trim().startsWith("$")) {
if (StringUtils.isEmpty(raw)) {
return new String[]{};
}
return raw.split(",");
} else {
raw = raw.trim();
String packages = this.environment.getProperty(raw.substring("${".length(), raw.length() - "}".length()));
return packages.split(",");
}
}
}
How to use?Here is configuration file.
PrimaryJpaConfiguration.java
import com.shinow.abd.springjpa2.annotation.AbdEnableJpaRepositories;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import java.util.Map;
#Configuration
#AbdEnableJpaRepositories(
basePackages = "${spring.jpa.base-packages}",
entityManagerFactoryRef = "entityManagerFactory",
transactionManagerRef = "transactionManager"
)
#EnableAutoConfiguration(exclude = HibernateJpaAutoConfiguration.class)
public class PrimaryJpaConfiguration implements EnvironmentAware {
private Environment env;
#Bean
#ConditionalOnMissingBean(name = "entityManager")
#Primary
public EntityManager entityManager(#Qualifier("entityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
return entityManagerFactory.getObject().createEntityManager();
}
#Bean
#ConditionalOnMissingBean(name = "entityManagerFactory")
#Primary
public LocalContainerEntityManagerFactoryBean entityManagerFactory(#Qualifier("dataSource") DataSource dataSource) {
Map<String, Object> properties = JpaProperties.get("", env);
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource);
entityManagerFactoryBean.setJpaPropertyMap(properties);
entityManagerFactoryBean.setPackagesToScan(env.getProperty("spring.jpa.base-packages").split(","));
entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
return entityManagerFactoryBean;
}
#Bean
#ConditionalOnMissingBean(name = "dataSource")
#Primary
#ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
#Bean
#ConditionalOnMissingBean(name = "transactionManager")
#Primary
public PlatformTransactionManager transactionManager(#Qualifier("entityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
JpaTransactionManager transactionManager
= new JpaTransactionManager();
transactionManager.setEntityManagerFactory(
entityManagerFactory.getObject());
return transactionManager;
}
#Override
public void setEnvironment(Environment environment) {
this.env = environment;
}
}
You can add spring.jpa.base-packages config to your application.properties.
For example:spring.jpa.base-packages=com.foo.a,com.bar.b,and the repositories and entities under those packages "com.foo.a" and "com.bar.b" will be added to the spring Ioc container.
Answer by 高慧觉 Spring Data JPA - How to programmatically set JpaRepository base packages worked for me, but I've come up with a simpler and more reliable AnnotationRepositoryConfigurationSource implementation: just allow Spring Data to collect the packages in its way, and then post-process them and expand property placeholders to package names.
class AbdAnnotationRepositoryConfigurationSource extends AnnotationRepositoryConfigurationSource {
private final Environment environment;
ExpressionsSupportingAnnotationRepositoryConfigurationSource(AnnotationMetadata metadata, Class<? extends Annotation> annotation,
ResourceLoader resourceLoader, Environment environment, BeanDefinitionRegistry registry) {
super(metadata, annotation, resourceLoader, environment, registry);
this.environment = environment;
}
#Override
public Streamable<String> getBasePackages() {
Streamable<String> rawPackages = super.getBasePackages();
return Streamable.of(() -> rawPackages.stream()
.flatMap(raw -> parsePackagesSpel(raw).stream())
);
}
private List<String> parsePackagesSpel(#Nullable String rawPackage) {
Objects.requireNonNull(rawPackage, "Package specification cannot be null");
if (!rawPackage.trim().startsWith("$")) {
return Collections.singletonList(rawPackage);
}
rawPackage = rawPackage.trim();
String propertyName = rawPackage.substring("${".length(), rawPackage.length() - "}".length());
String packages = this.environment.getProperty(propertyName);
if (!StringUtils.hasText(packages)) {
throw new IllegalStateException(
String.format("Could not resolve the following packages definition: %s", rawPackage));
}
return Arrays.stream(packages.split(","))
.map(String::trim)
.filter(StringUtils::hasText)
.collect(Collectors.toList());
}
}
I've implemented a way to invoke #EnableJpaRepositories purely programmatically. For the purpose I've created a class which simulates the Annotation data passed to a config class annotated with the #EnableJpaRepositories annotation. I called my class EnableJpaRepositoriesData. It is confirmed working with Spring 5.1.5, SpringBoot 2.1.3. I guess it must be compatible with earlier and later versions alike, with none or very little changes at most.
Below follows the class code, and a sample usage code for it.
EnableJpaRepositoriesData.java:
package org.patladj.jpa.config.multijpa;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Indexed;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.TransactionManagementConfigurationSelector;
import java.lang.reflect.Method;
import java.util.*;
public class EnableJpaRepositoriesData extends AnnotationMetadataReadingVisitor implements AnnotationMetadata {
private Map<String, ?> data;
private void initIt() throws NoSuchMethodException, ClassNotFoundException {
//################## protected final Set<String> annotationSet = new LinkedHashSet<>(4);
annotationSet.add(Configuration.class.getCanonicalName());
annotationSet.add(EnableTransactionManagement.class.getCanonicalName());
annotationSet.add(EnableJpaRepositories.class.getCanonicalName());
//################## protected final Map<String, Set<String>> metaAnnotationMap = new LinkedHashMap<>(4);
metaAnnotationMap.put(Configuration.class.getCanonicalName(),
new LinkedHashSet<>(Arrays.asList(
Component.class.getCanonicalName(),
Indexed.class.getCanonicalName()
)));
metaAnnotationMap.put(EnableTransactionManagement.class.getCanonicalName(),
new LinkedHashSet<>(Arrays.asList(
Import.class.getCanonicalName()
)));
metaAnnotationMap.put(EnableJpaRepositories.class.getCanonicalName(),
new LinkedHashSet<>(Arrays.asList(
Import.class.getCanonicalName()
)));
//################## protected final LinkedMultiValueMap<String, AnnotationAttributes> attributesMap = new LinkedMultiValueMap<>(4);
attributesMap.put(Configuration.class.getCanonicalName(),
new LinkedList<AnnotationAttributes>() {{
add(new AnnotationAttributes(new LinkedHashMap<String, Object>() {{
put("value", defaultFor(Configuration.class, "value"));
}}));
}});
attributesMap.put(Component.class.getCanonicalName(),
new LinkedList<AnnotationAttributes>() {{
add(new AnnotationAttributes(new LinkedHashMap<String, Object>() {{
put("value", defaultFor(Component.class, "value"));
}}));
}});
attributesMap.put(Indexed.class.getCanonicalName(),
new LinkedList<AnnotationAttributes>() {{
add(new AnnotationAttributes(new LinkedHashMap<String, Object>() {{
}}));
}});
attributesMap.put(EnableTransactionManagement.class.getCanonicalName(),
new LinkedList<AnnotationAttributes>() {{
add(new AnnotationAttributes(new LinkedHashMap<String, Object>() {{
put("order", defaultFor(EnableTransactionManagement.class, "order"));
put("mode", defaultFor(EnableTransactionManagement.class, "mode"));
put("proxyTargetClass", defaultFor(EnableTransactionManagement.class, "proxyTargetClass"));
}}));
}});
attributesMap.put(Import.class.getCanonicalName(),
new LinkedList<AnnotationAttributes>() {{
add(new AnnotationAttributes(new LinkedHashMap<String, Object>() {{
put("value", new Class<?>[]{TransactionManagementConfigurationSelector.class});
}}));
add(new AnnotationAttributes(new LinkedHashMap<String, Object>() {{
put("value", new Class<?>[]{Class.forName("org.springframework.data.jpa.repository.config.JpaRepositoriesRegistrar")});
}}));
}});
attributesMap.put(EnableJpaRepositories.class.getCanonicalName(),
new LinkedList<AnnotationAttributes>() {{
add(new AnnotationAttributes(new LinkedHashMap<String, Object>() {{
put("repositoryBaseClass", data.get("repositoryBaseClass"));
put("basePackages", data.get("basePackages"));
put("value", defaultFor(EnableJpaRepositories.class, "value"));
put("excludeFilters", new AnnotationAttributes[]{});
put("includeFilters", new AnnotationAttributes[]{});
put("basePackageClasses", defaultFor(EnableJpaRepositories.class, "basePackageClasses"));
put("bootstrapMode", defaultFor(EnableJpaRepositories.class, "bootstrapMode"));
put("transactionManagerRef", data.get("transactionManagerRef"));
put("considerNestedRepositories", defaultFor(EnableJpaRepositories.class, "considerNestedRepositories"));
put("namedQueriesLocation", defaultFor(EnableJpaRepositories.class, "namedQueriesLocation"));
put("queryLookupStrategy", defaultFor(EnableJpaRepositories.class, "queryLookupStrategy"));
put("entityManagerFactoryRef", data.get("entityManagerFactoryRef"));
put("enableDefaultTransactions", defaultFor(EnableJpaRepositories.class, "enableDefaultTransactions"));
put("repositoryImplementationPostfix", defaultFor(EnableJpaRepositories.class, "repositoryImplementationPostfix"));
put("repositoryFactoryBeanClass", defaultFor(EnableJpaRepositories.class, "repositoryFactoryBeanClass"));
}}));
}});
//##################
}
public EnableJpaRepositoriesData(#Nullable ClassLoader classLoader, Map<String, ?> data) throws NoSuchMethodException, ClassNotFoundException {
super(classLoader);
this.data = data;
this.initIt();
}
private Object defaultFor(Class<?> clazz, String methodName) throws NoSuchMethodException {
Method method = clazz.getDeclaredMethod(methodName);
return method.getDefaultValue();
}
}
Sample usage of the Class to actually invoke enableJpaRepositories
programmatically:
AnnotationMetadata enableJpaRepositoriesData = null;
try {
enableJpaRepositoriesData = new EnableJpaRepositoriesData(this.getClass().getClassLoader(), new HashMap<String, Object>() {{
put("repositoryBaseClass", MyJPAConnectorImpl.class);
put("basePackages", new String[] {
//This is where you set the repositories base packages to scan.
//... Having your best programmatic convenience
"org.patladj.connector.multijpa.common",
"org.patladj.connector.multijpa.anotherrepositoriespackage",
"org.patladj.connector.multijpa.morepackagestoscanforrepositories..."
});
//Ofcourse you need to create by yourself the custom transactionManagerFactory,
// ... entityManagerFactory, JPAVendorAdapter and Datasource beans for your
// ... custom persistence contexts using unique bean names (different than
// ... the default Spring data names: 'entityManagerFactory', etc...)
// ... Then "send" these bean names as references to each and every ...
// and as many as you want persistence contexts programmatically here
put("transactionManagerRef", myCustomPersistanceContextName + "_transactionManager");
put("entityManagerFactoryRef", myCustomPersistanceContextName + "_entityManagerFactory");
}});
} catch (NoSuchMethodException e) {
throw new ExceptionInInitializerError(e);
} catch (ClassNotFoundException e) {
throw new ExceptionInInitializerError(e);
}
AnnotationRepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(
enableJpaRepositoriesData, EnableJpaRepositories.class, applicationContext, environment, registry
);
RepositoryConfigurationExtension extension = new JpaRepositoryConfigExtension();
RepositoryConfigurationUtils.exposeRegistration(extension, registry, configurationSource);
RepositoryConfigurationDelegate delegate = new RepositoryConfigurationDelegate(configurationSource, applicationContext, environment);
delegate.registerRepositoriesIn(registry, extension);
And finally, in case if you don't know, you can use the following #Configuration class
public class MultiJPABeanDefinitionRegistrar implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware
... to override 2 of its methods to have the post process bean registry and applicationContext at your disposal to register bean definitions dynamically.
Methods to override:
- public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
- public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException
I referred this post only and created a module aware multi database aware spring data library for mysql(compatible with spring boot) in github.Some application properties need to be added and you are done .
Documentation and other details could be found at :-
https://github.com/yatharthamishra0419/spring-boot-data-multimodule-mysql
What your looking for is #EntityScan but it's only available in Spring Boot. The configuration you can annotate in Spring Data JPA is documented here https://docs.spring.io/spring-data/jpa/docs/2.0.8.RELEASE/reference/html/#jpa.java-config
#Configuration
#EnableJpaRepositories
#EnableTransactionManagement
class ApplicationConfig {
#Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder.setType(EmbeddedDatabaseType.HSQL).build();
}
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(true);
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan("com.acme.domain");
factory.setDataSource(dataSource());
return factory;
}
#Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
I have to do a Spring Boot version 1.5 application that can do like this: it creates an object and try to persist to both data sources (example: 2 databases named: test_book_1 and test_book_2 in Postgresql).
I have found an example that could work for 2 different objects (Author: A, Book: B) which can be stored in different databases (A goes to test_book_1 and B goes to test_book_2). This is a good example but it is not what I wanted.
Store separate objects to different data sources
I got the idea that I need to define 2 custom JPA DatabaseConfigurations and need to config them to manage the same repository and domain class. However, Spring only use the second class as Qualifier to inject for JPA repository (I understand that when both configurations point to same class then the second one can override).
The question is, how can I tell Spring to let it knows that when it should inject the correct Bean (BookRepository) from the wanted data source (I wanted to persist the object to both data sources, not just the second one).
Here is the modified code from the example link above.
An application.properties file which is modified to create 2 database in Postgresql instead of 1 in Postgresql and 1 in Mysql.
server.port=8082
# -----------------------
# POSTGRESQL DATABASE CONFIGURATION
# -----------------------
spring.postgresql.datasource.url=jdbc:postgresql://localhost:5432/test_book_db
spring.postgresql.datasource.username=petauser
spring.postgresql.datasource.password=petapasswd
spring.postgresql.datasource.driver-class-name=org.postgresql.Driver
# ------------------------------
# POSTGRESQL 1 DATABASE CONFIGURATION
# ------------------------------
spring.mysql.datasource.url=jdbc:postgresql://localhost:5432/test_author_db
spring.mysql.datasource.username=petauser
spring.mysql.datasource.password=petapasswd
spring.mysql.datasource.driver-class-name=org.postgresql.Driver
package: com.roufid.tutorial.configuration
class APostgresqlConfiguration
package com.roufid.tutorial.configuration;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.roufid.tutorial.entity.postgresql.Book;
/**
* Spring configuration of the "PostgreSQL" database.
*
* #author Radouane ROUFID.
*
*/
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = "postgresqlEntityManager",
transactionManagerRef = "postgresqlTransactionManager",
basePackages = "com.roufid.tutorial.dao.postgresql"
)
public class APostgresqlConfiguration {
/**
* PostgreSQL datasource definition.
*
* #return datasource.
*/
#Bean
#Primary
#ConfigurationProperties(prefix = "spring.postgresql.datasource")
public DataSource postgresqlDataSource() {
return DataSourceBuilder
.create()
.build();
}
/**
* Entity manager definition.
*
* #param builder an EntityManagerFactoryBuilder.
* #return LocalContainerEntityManagerFactoryBean.
*/
#Primary
#Bean(name = "postgresqlEntityManager")
public LocalContainerEntityManagerFactoryBean postgresqlEntityManagerFactory(EntityManagerFactoryBuilder builder) {
return builder
.dataSource(postgresqlDataSource())
.properties(hibernateProperties())
.packages(Book.class)
.persistenceUnit("postgresqlPU")
.build();
}
#Primary
#Bean(name = "postgresqlTransactionManager")
public PlatformTransactionManager postgresqlTransactionManager(#Qualifier("postgresqlEntityManager") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
private Map<String, Object> hibernateProperties() {
Resource resource = new ClassPathResource("hibernate.properties");
try {
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
return properties.entrySet().stream()
.collect(Collectors.toMap(
e -> e.getKey().toString(),
e -> e.getValue())
);
} catch (IOException e) {
return new HashMap<String, Object>();
}
}
}
package: com.roufid.tutorial.configuration
class MysqlConfiguration
package com.roufid.tutorial.configuration;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.roufid.tutorial.entity.mysql.Author;
import com.roufid.tutorial.entity.postgresql.Book;
/**
* Spring configuration of the "mysql" database.
*
* #author Radouane ROUFID.
*
*/
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = "mysqlEntityManager",
transactionManagerRef = "mysqlTransactionManager",
basePackages = "com.roufid.tutorial.dao.postgresql"
)
public class MysqlConfiguration {
/**
* MySQL datasource definition.
*
* #return datasource.
*/
#Bean
#ConfigurationProperties(prefix = "spring.mysql.datasource")
public DataSource mysqlDataSource() {
return DataSourceBuilder
.create()
.build();
}
/**
* Entity manager definition.
*
* #param builder an EntityManagerFactoryBuilder.
* #return LocalContainerEntityManagerFactoryBean.
*/
#Bean(name = "mysqlEntityManager")
public LocalContainerEntityManagerFactoryBean mysqlEntityManagerFactory(EntityManagerFactoryBuilder builder) {
return builder
.dataSource(mysqlDataSource())
.properties(hibernateProperties())
.packages(Book.class)
.persistenceUnit("mysqlPU")
.build();
}
/**
* #param entityManagerFactory
* #return
*/
#Bean(name = "mysqlTransactionManager")
public PlatformTransactionManager mysqlTransactionManager(#Qualifier("mysqlEntityManager") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
private Map<String, Object> hibernateProperties() {
Resource resource = new ClassPathResource("hibernate.properties");
}
} try {
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
return properties.entrySet().stream()
.collect(Collectors.toMap(
e -> e.getKey().toString(),
e -> e.getValue())
);
} catch (IOException e) {
return new HashMap<String, Object>();
}
}
}
package com.roufid.tutorial.dao.postgresql
class BookRepository
package com.roufid.tutorial.dao.postgresql;
import org.springframework.data.repository.CrudRepository;
import com.roufid.tutorial.entity.postgresql.Book;
/**
* Book repository.
*
* #author Radouane ROUFID.
*
*/
public interface BookRepository extends CrudRepository<Book, Long> {
}
package com.roufid.tutorial.entity.postgresql
class Book
package com.roufid.tutorial.entity.postgresql;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
#Entity
#Table(name = "BOOK")
public class Book implements Serializable {
private static final long serialVersionUID = -9019470250770543773L;
#Id
private Long id;
#Column
private String name;
#Column
private Long authorId;
...
// Setters, Getters
}
And a test class to inject the BookRepository which will use the MysqlConfiguration class (second datasource) only.
#RunWith(SpringRunner.class)
#SpringBootTest
public class ApplicationTest {
#Autowired
private BookRepository bookRepository;
#Before
public void init() {
Book book = new Book();
book.setId(bookId);
book.setName("Spring Boot Book");
// How can it persist to the first datasource?
bookRepository.save(book);
}
}
Looks like you need multitenancy support.
There is a Spring based solution for this
You need to implement CurrentTenantIdentifierResolver interface
public String resolveCurrentTenantIdentifier()
And extend
AbstractDataSourceBasedMultiTenantConnectionProviderImpl
to return DataSource for the tenant
See more here
So I think I got an answer myself (I want to stick with Spring JPA and Hibernate only). So here is what I did, inspired from Spring Booth with 2 different data sources
The most important class is the config class to manually create 2 data sources (2 databases in Postgresql)
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = "sourceEntityManagerFactory",
basePackages = "application"
)
public class PersistenceConfig {
#Autowired
private JpaVendorAdapter jpaVendorAdapter;
private String databaseUrl = "jdbc:postgresql://localhost:5432/test_book_db";
private String targetDatabaseUrl = "jdbc:postgresql://localhost:5432/test_author_db";
private String username = "petauser";
private String password = "petapasswd";
private String driverClassName = "org.postgresql.Driver";
private String dialect = "org.hibernate.dialect.PostgreSQLDialect";
private String ddlAuto = "update";
#Bean
public EntityManager sourceEntityManager() {
return sourceEntityManagerFactory().createEntityManager();
}
#Bean
public EntityManager targetEntityManager() {
return targetEntityManagerFactory().createEntityManager();
}
#Bean
#Primary
public EntityManagerFactory sourceEntityManagerFactory() {
return createEntityManagerFactory("source", databaseUrl);
}
#Bean
public EntityManagerFactory targetEntityManagerFactory() {
return createEntityManagerFactory("target", targetDatabaseUrl);
}
#Bean(name = "transactionManager")
#Primary
public PlatformTransactionManager sourceTransactionManager() {
return new JpaTransactionManager(sourceEntityManagerFactory());
}
#Bean
public PlatformTransactionManager targetTransactionManager() {
return new JpaTransactionManager(targetEntityManagerFactory());
}
private EntityManagerFactory createEntityManagerFactory(final String persistenceUnitName,
final String databaseUrl) {
final LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
final DriverManagerDataSource dataSource = new DriverManagerDataSource(databaseUrl, username, password);
dataSource.setDriverClassName(driverClassName);
entityManagerFactory.setDataSource(dataSource);
entityManagerFactory.setJpaVendorAdapter(jpaVendorAdapter);
entityManagerFactory.setPackagesToScan("application.domain");
entityManagerFactory.setPersistenceUnitName(persistenceUnitName);
final Properties properties = new Properties();
properties.setProperty("hibernate.dialect", dialect);
properties.setProperty("hibernate.hbm2ddl.auto", ddlAuto);
entityManagerFactory.setJpaProperties(properties);
entityManagerFactory.afterPropertiesSet();
return entityManagerFactory.getObject();
}
}
Because of I want to copy a stored entity from source database to a target database. So I used Spring JPA to read the object from source database
public interface StorageEntryRepository extends CrudRepository<StorageEntry, Long> {
}
And I made a service class to check if the entity which is existed by value (someValue contain a substring "Book") in target database before persisting it in target database by Hibernate (the StorageEntry here is a domain class from the example link above).
#Service
#Transactional(rollbackFor = Exception.class)
public class StorageEntryService {
#Autowired
private StorageEntryRepository storageEntryRepository;
#PersistenceContext(unitName = "target")
private EntityManager targetEntityManager;
public void save(StorageEntry storageEntry) throws Exception {
// this.storageEntryRepository.save(storageEntry);
// Load an stored entry from the source database
StorageEntry storedEntry = this.storageEntryRepository.findOne(12L);
//this.storageEntryRepository.save(storageEntry);
// Save also to a different database
final Session targetHibernateSession = targetEntityManager.unwrap(Session.class);
Criteria criteria = targetHibernateSession.createCriteria(StorageEntry.class);
criteria.add(Restrictions.like("someValue", "%Book1%"));
List<StorageEntry> storageEntries = criteria.list();
if (storageEntries.isEmpty()) {
targetEntityManager.merge(storedEntry);
// No flush then nodata is saved in the different database
targetHibernateSession.flush();
System.out.println("Stored the new object to target database.");
} else {
System.out.println("Object already existed in target database.");
}
}
}
So it ends up with I can use both JPA from the current working application and just need to make another application with a config class and a service class to do this migration of existing objects to a new database.
I'm fairly new to Spring (the Neo4j side), and I am having trouble #AutoWire-ing my repository.
this is my repo:
package org.jarivm.relationGraph.objects.repositories;
public interface EmployeeRepository extends GraphRepository<Employee> {
#Query("MATCH a=(:Employee)-[:WORKED_ON]->(p:Project) WHERE id(p)={0} RETURN a")
Iterable<Employee> getTeamMates(Project client);
}
my test class:
package org.jarivm.relationGraph;
import org.apache.commons.collections4.set.ListOrderedSet;
import org.jarivm.relationGraph.objects.domains.Employee;
import org.jarivm.relationGraph.objects.domains.Project;
import org.jarivm.relationGraph.objects.repositories.EmployeeRepository;
import org.jarivm.relationGraph.utilities.NodeProperties;
import org.junit.After;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Iterator;
/**
* #author Jari Van Melckebeke
* #since 02.09.16
*/
#FixMethodOrder(MethodSorters.JVM)
public class Tests extends Application {
#Autowired
private Facade facade;
#Autowired
private EmployeeRepository employeeRepository;
#Before
public void setUp() throws Exception {
facade = new Facade();
}
#After
public void tearDown() throws Exception {
facade.tearDown();
}
/*
#Test
public void persistedEmployeeShouldBeRetrievableFromGraphDB() {
Employee employee = new Employee("john", "adams");
//System.out.println(session.getTransaction().status());
if (!facade.findEmployeeByProperty("name", employee.getName()).iterator().hasNext()) {
facade.commit(employee);
Employee foundHim = facade.findEmployeeByProperty("name", employee.getName()).iterator().next();
assert foundHim.getId().equals(employee.getId());
assert foundHim.getName().equals(employee.getName());
}
}
#Test
public void persistedChainShouldBeRetrievableFromGraphDB() {
Employee employee = new Employee("john", "myles");
Client client = new Client();
Sector sector = new Sector();
Project project = new Project();
client.setName("Real Dolmen");
project.setClient(client);
project.setCost(100.0);
project.setName("project highrise");
Set<Employee> set = new ListOrderedSet<Employee>();
set.add(employee);
project.setTeam(set);
sector.setName("game");
client.setSector(sector);
facade.commit(sector);
facade.commit(employee);
facade.commit(client);
facade.commit(project);
Client foundHim = facade.findClientByProperty("name", client.getName()).iterator().next();
assert foundHim.getId().equals(client.getId());
assert foundHim.getName().equals(client.getName());
}
#Test
public void projectShouldBeInsertableAlone() {
Project project = new Project();
project.setName("random");
project.setLanguage("Java");
facade.commit(project);
Project foundHim = facade.findProjectByProperty("name", project.getName()).iterator().next();
assert foundHim.getId().equals(project.getId());
}
#Test
public void clientShouldBeInsertableAlone() {
Client client = new Client();
client.setName("Colruyt");
facade.commit(client);
Client foundHim = facade.findClientByProperty("name", client.getName()).iterator().next();
assert foundHim.getId().equals(client.getId());
}*/
#Test
public void createdNodesShoudBeEditable() {
Iterator<Employee> employees = facade.findEmployeeByProperty("name", "john").iterator();
Project project = facade.findProjectByProperty("name", "random").iterator().next();
while (employees.hasNext()) {
Employee e = employees.next();
if (project.getTeam() == null)
project.setTeam(new ListOrderedSet<Employee>());
project.getTeam().add(e);
}
facade.commit(project);
}
package org.jarivm.relationGraph;
#Autowired
private EmployeeRepository employeeRepository;
#Test
public void teamMatesShouldBeViewable() {
Project p = facade.findProjectByProperty("name", "Matsoft").iterator().next();
System.out.println(p);
System.out.println(employeeRepository);
Iterable<Employee> e = employeeRepository.getTeamMates(p);
System.out.println(e.iterator());
}
}
and my Application.java class:
package org.jarivm.relationGraph;
import org.neo4j.ogm.session.SessionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.config.Neo4jConfiguration;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* #author Jari Van Melckebeke
* #since 23.09.16
*/
#EnableTransactionManagement
#ComponentScan(basePackages = {"org.jarivm.relationGraph"})
#Configuration
#EnableNeo4jRepositories(basePackages = "org.jarivm.relationGraph.objects.repositories.EmployeeRepository")
public class Application extends Neo4jConfiguration {
public static final String URL = System.getenv("NEO4J_URL") != null ? System.getenv("NEO4J_URL") : "http://localhost:7474";
#Bean
public org.neo4j.ogm.config.Configuration getConfiguration() {
org.neo4j.ogm.config.Configuration config = new org.neo4j.ogm.config.Configuration();
config
.driverConfiguration()
.setDriverClassName("org.neo4j.ogm.drivers.http.driver.HttpDriver")
.setURI(URL)
.setCredentials("neo4j", "mypassword");
return config;
}
#Override
public SessionFactory getSessionFactory() {
return new SessionFactory(getConfiguration(), "org.jarivm.relationGraph.objects.domains");
}
}
The #autowire did never work with this program so I do not know what the problem is...
thank's in advance,
Jari Van Melckebeke
I think your Tests class should not extend Application, but instead be annotated with RunsWith - something like (untested):
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes=org.jarivm.relationGraph.Application.class, loader=AnnotationConfigContextLoader.class
public class Tests {
for more information, see the section titled Integration Testing with #Configuration Classes:
https://spring.io/blog/2011/06/21/spring-3-1-m2-testing-with-configuration-classes-and-profiles
I have the following DateTimeTypeHandler class:
//Copyright 2012 Lucas Libraro
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.common.mybatis.typehandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import java.sql.*;
#MappedTypes(DateTime.class)
public class DateTimeTypeHandler implements TypeHandler {
/* (non-Javadoc)
* #see org.apache.ibatis.type.TypeHandler#setParameter(java.sql.PreparedStatement, int, java.lang.Object, org.apache.ibatis.type.JdbcType)
*/
public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
if (parameter != null) {
ps.setTimestamp(i, new Timestamp(((DateTime) parameter).getMillis()));
} else {
ps.setTimestamp(i, null);
}
}
/* (non-Javadoc)
* #see org.apache.ibatis.type.TypeHandler#getResult(java.sql.ResultSet, java.lang.String)
*/
public Object getResult(ResultSet rs, String columnName) throws SQLException {
Timestamp ts = rs.getTimestamp(columnName);
if (ts != null) {
return new DateTime(ts.getTime(), DateTimeZone.UTC);
} else {
return null;
}
}
/* (non-Javadoc)
* #see org.apache.ibatis.type.TypeHandler#getResult(java.sql.CallableStatement, int)
*/
public Object getResult(CallableStatement cs, int columnIndex) throws SQLException {
Timestamp ts = cs.getTimestamp(columnIndex);
if (ts != null) {
return new DateTime(ts.getTime(), DateTimeZone.UTC);
} else {
return null;
}
}
/* (non-Javadoc)
* #see org.apache.ibatis.type.TypeHandler#getResult(java.sql.ResultSet, int)
*/
public Object getResult(ResultSet rs, int columnIndex) throws SQLException {
Timestamp ts = rs.getTimestamp(columnIndex);
if (ts != null) {
return new DateTime(ts.getTime(), DateTimeZone.UTC);
} else {
return null;
}
}
}
I am able to serialiaze some Java to Json, Json to java using the following beans :
DataBean
package com.common.config;
import com.common.business.user.Employee;
import com.common.config.yaml.DatabaseConfig;
import com.common.mybatis.typehandler.DateTimeTypeHandler;
import com.commonTools.model.DBObject;
import com.commonTools.model.Snoop;
import com.commonTools.model.SnoopDBObject;
import org.apache.ibatis.type.TypeHandler;
import org.joda.time.DateTime;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
#Configuration
#Import(DatabaseConfig.class)
#MapperScan("com.persistence")
public class DataBean {
#Autowired DatabaseConfig _databaseConfig;
#Bean
public DateTimeTypeHandler dateTimeTypeHandler(){
return new DateTimeTypeHandler();
}
#Bean
public SimpleDriverDataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSource.setDriverClass(org.mariadb.jdbc.Driver.class);
dataSource.setUsername(_databaseConfig.getUsername());
dataSource.setPassword(_databaseConfig.getPassword());
dataSource.setUrl(_databaseConfig.getUrl());
return dataSource;
}
#Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
#Bean
public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource());
sessionFactory.setTypeHandlers(new TypeHandler[] {
new DateTimeTypeHandler(),
});
sessionFactory.setTypeAliases(new Class[] {
DateTime.class,
Employee.class,
DBObject.class,
Snoop.class,
SnoopDBObject.class
});
return sessionFactory;
}
}
and JacksonBean
package com.common.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class JacksonBean {
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
// # TEST 0 (TODO: see result when activated using JodaTime)
mapper.registerModule(new JodaModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS );
// mapper.setDateFormat(new SimpleDateFormat("EEE MMM dd yyyy HH:mm:ss 'GMT'ZZZ (z)"));
// # TEST 1 (Res: fail sometimes when too much depth)
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); // true default
// # TEST 2 (Res: too much depth)
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
.setVisibility(PropertyAccessor.CREATOR, JsonAutoDetect.Visibility.ANY)
.setVisibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.NONE)
.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE)
.setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE);
return mapper;
}
}
This work well, I am able to serialize the DateTime object but this does not work when I execute the following EmployeeServiceTest :
package com.common.service.user;
import com.common.business.user.Employee;
import com.common.config.DataBean;
import com.commonTools.SessionIdentifierGenerator;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.ConfigFileApplicationContextInitializer;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
import static org.junit.Assert.*;
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(initializers = ConfigFileApplicationContextInitializer.class, classes = {DataBean.class, EmployeeService.class})
public class EmployeeServiceTest implements EmployeeServiceDAO {
private static final String SAMPLE_NAME = "JUnitTestUser_";
#Autowired
private EmployeeService _employeeService;
private String getRandomName(){
return SAMPLE_NAME + new SessionIdentifierGenerator().nextSessionId();
}
#Test
public void test_getAll(){
List<Employee> employeeList = this.getAll();
Employee testEmployee = new Employee();
testEmployee.setFirstName(getRandomName());
testEmployee.setLastName(getRandomName());
this.save(testEmployee);
assertNotNull("should have an id", testEmployee.getId());
List<Employee> employeeListIncremented = this.getAll();
assertEquals("Should have +1 in list", employeeList.size() + 1 , employeeListIncremented.size());
this.removeById(testEmployee.getId());
assertTrue("should be deleted", this.getById(testEmployee.getId()) == null);
}
#Test
public void test_getById(){
Employee testEmployee = new Employee();
testEmployee.setFirstName(getRandomName());
testEmployee.setLastName(getRandomName());
this.save(testEmployee);
assertNotNull("should have an id", testEmployee.getId());
Employee employeeById = this.getById(testEmployee.getId());
assertEquals("should be equal id", employeeById.getId(), testEmployee.getId());
this.removeById(testEmployee.getId());
assertTrue("should be deleted", this.getById(testEmployee.getId()) == null);
}
#Test
public void test_getFromFirstName(){
Employee testEmployee = new Employee();
testEmployee.setFirstName(getRandomName());
testEmployee.setLastName(getRandomName());
this.save(testEmployee);
assertNotNull("should have an id", testEmployee.getId());
Employee employeeFromFirstName = this.getFromFirstName(testEmployee.getFirstName());
assertEquals("should be equal id", employeeFromFirstName.getId(), testEmployee.getId());
this.removeById(testEmployee.getId());
assertTrue("should be deleted", this.getById(testEmployee.getId()) == null);
}
#Test
public void test_save() {
Employee testEmployee = new Employee();
testEmployee.setFirstName(getRandomName());
testEmployee.setLastName(getRandomName());
this.save(testEmployee);
assertNotNull("should have an id", testEmployee.getId());
// assertTrue("should have a creation datetime", null != testEmployee.getCreation().getDateTime() );
String firstNameTmp = getRandomName();
testEmployee.setFirstName(firstNameTmp);
this.save(testEmployee);
Employee editedEmployee = this.getById(testEmployee.getId());
assertEquals("should have first name \""+firstNameTmp+"\"", firstNameTmp, editedEmployee.getFirstName());
this.removeById(testEmployee.getId());
assertTrue("should be deleted", this.getById(testEmployee.getId()) == null);
}
#Test
public void test_removeById() {
test_save();
}
public List<Employee> getAll() {
return _employeeService.getAll();
}
public Employee getById(Integer id) {
return _employeeService.getById(id);
}
public Employee getFromFirstName(String firstName) {
return _employeeService.getFromFirstName(firstName);
}
public Employee save(Employee employee) {
_employeeService.save(employee);
return employee;
}
public void removeById(Integer id) {
_employeeService.removeById(id);
}
}
These questions comes in my mind :
It's not very handy to serialize #Configuration class like DatabaseConfig, this because Spring also include it's own properties in the serialization chain.
The UnitTest fail because of // assertTrue("should have a creation datetime", null != testEmployee.getCreation().getDateTime() );, in the test , getCreation is null while it shouldn't, how should I initialize the Jackson to DateTime Jodatime mapping during the test .
I also notice the test doesn't rollback the database when it fails. I used to do Unit Testing with javascript and I was used to the before(), after() method that do post install and pre install of the test, is there a conveniant way to achieve that.
Beside all I have done here, is this the most appropriate way to design a RESTful api ?