Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Customizing Spring Data repository bean names for use with multiple data sources

I have a project that utilizes Spring Data (MongoDB in this instance) to interact with multiple databases with the same schema. What this means is that each database utilizes the same entity and repository classes. So, for example:

public class Thing {
    private String id;
    private String name;
    private String type;
    // etc...  
}

public interface ThingRepository extends PagingAndSortingRepository<Thing, String> {
    List<Thing> findByName(String name);
}

@Configuration
@EnableMongoRepositories(basePackageClasses = { ThingRepository.class })
public MongoConfig extends AbstractMongoConfiguration {
    // Standard mongo config
}

This works fine if I am connecting to a single database, but things get more complicated when I want to connect to more than one database at the same time:

@Configuration
@EnableMongoRepositories(basePackageClasses = { ThingRepository.class },
    mongoTemplateRef = "mongoTemplateOne")
public MongoConfigOne extends AbstractMongoConfiguration {

    @Override
    @Bean(name = "mongoTemplateOne")
    public MongoTemplate mongoTemplate() throws Exception {
        return new MongoTemplate(this.mongo(), "db_one");
    }

    // Remaining standard mongo config

}

@Configuration
@EnableMongoRepositories(basePackageClasses = { ThingRepository.class },
    mongoTemplateRef = "mongoTemplateTwo")
public MongoConfigTwo extends AbstractMongoConfiguration {

    @Override
    @Bean(name = "mongoTemplateTwo")
    public MongoTemplate mongoTemplate() throws Exception {
        return new MongoTemplate(this.mongo(), "db_two");
    }

    // Remaining standard mongo config

}

I can create multiple instances of the same repository, using different MongoTemplate instances, but I don't know the correct way to reference and inject them. I would like to be able to inject the individual repository instances into different controllers, like so:

@Controller
@RequestMapping("/things/one/")
public class ThingOneController {
    @Resource private ThingRepository thingRepositoryOne;
    ...
}

@Controller
@RequestMapping("/things/two/")
public class ThingTwoController {
    @Resource private ThingRepository thingRepositoryTwo;
    ...
}

Is a configuration like this possible? Can I somehow control the bean names of the instantiated interfaces so that I can reference them with @Resource or @Autowired?

Bonus question: can this be accomplished with a custom repository factory as well?

like image 325
woemler Avatar asked Jul 12 '16 19:07

woemler


2 Answers

Create your repository interface with @NoRepositoryBean, we'll wire it up ourself:

@NoRepositoryBean
public interface ModelMongoRepository extends MongoRepository<Model, String> {
}      

Then, in a @Configuration class, instantiate the 2 repository beans using MongoRepositoryFactoryBean. Both repositories will return the same Spring Data Repository interface, but we'll assign them different MongoOperations (ie: database details):

@Configuration
@EnableMongoRepositories
public class MongoConfiguration {

    @Bean
    @Qualifier("one")
    public ModelMongoRepository modelMongoRepositoryOne() throws DataAccessException, Exception {
        MongoRepositoryFactoryBean<ModelMongoRepository, Model, String> myFactory = new MongoRepositoryFactoryBean<ModelMongoRepository, Model, String>();
        myFactory.setRepositoryInterface(ModelMongoRepository.class);
        myFactory.setMongoOperations(createMongoOperations("hostname1", 21979, "dbName1", "username1", "password1"));
        myFactory.afterPropertiesSet();
        return myFactory.getObject();
    }

    @Bean
    @Qualifier("two")
    public ModelMongoRepository modelMongoRepositoryTwo() throws DataAccessException, Exception {
        MongoRepositoryFactoryBean<ModelMongoRepository, Model, String> myFactory = new MongoRepositoryFactoryBean<ModelMongoRepository, Model, String>();
        myFactory.setRepositoryInterface(ModelMongoRepository.class);
        myFactory.setMongoOperations(createMongoOperations("hostname2", 21990, "dbName2", "username2", "password2"));
        myFactory.afterPropertiesSet();
        return myFactory.getObject();
    }

    private MongoOperations createMongoOperations(String hostname, int port, String dbName, String user, String pwd) throws DataAccessException, Exception {
        MongoCredential mongoCredentials = MongoCredential.createScramSha1Credential(user, dbName, pwd.toCharArray());
        MongoClient mongoClient = new MongoClient(new ServerAddress(hostname, port), Arrays.asList(mongoCredentials));
        Mongo mongo = new SimpleMongoDbFactory(mongoClient, dbName).getDb().getMongo();
        return new MongoTemplate(mongo, dbName);
    }
    //or this one if you have a connection string
    private MongoOperations createMongoOperations(String dbConnection) throws DataAccessException, Exception {
        MongoClientURI mongoClientURI = new MongoClientURI(dbConnection);
        MongoClient mongoClient = new MongoClient(mongoClientURI);
        Mongo mongo = new SimpleMongoDbFactory(mongoClient, mongoClientURI.getDatabase()).getDb().getMongo();
        return new MongoTemplate(mongo, mongoClientURI.getDatabase());
    }
}

You now have 2 beans with distinct @Qualifier names, each configured for different databases, and using the same model.

You can inject them using @Qualifier :

@Autowired
@Qualifier("one")
private ModelMongoRepository mongoRepositoryOne;

@Autowired
@Qualifier("two")
private ModelMongoRepository mongoRepositoryTwo;

For simplicity, I've hard coded the values in the configuration class, but you can inject them from properties in application.properties/yml.

EDIT to answer comments:

Here's the modification if you want to create a custom implementation without loosing the benefits of spring data interface repositories. the specs says this:

Often it is necessary to provide a custom implementation for a few repository methods. Spring Data repositories easily allow you to provide custom repository code and integrate it with generic CRUD abstraction and query method functionality. To enrich a repository with custom functionality you first define an interface and an implementation for the custom functionality. Use the repository interface you provided to extend the custom interface. The most important bit for the class to be found is the Impl postfix of the name on it compared to the core repository interface (see below).

Create a new interface, which has technically nothing to do with spring data, good old interface:

public interface CustomMethodsRepository {
    public void getById(Model model){
}

Have your repository interface extends this new interface:

@NoRepositoryBean
public interface ModelMongoRepository extends MongoRepository<Model, String>, CustomMethodsRepository {
} 

Then, create your implementation class, which only implements your non-spring-data interface:

public class ModelMongoRepositoryImpl  implements CustomModelMongoRepository {
    private MongoOperations mongoOperations;

    public ModelMongoRepositoryImpl(MongoOperations mongoOperations) {
        this.mongoOperations = mongoOperations;
    }
    public void getById(Model model){
        System.out.println("test");
    }
}

Change the Java configuration to add myFactory.setCustomImplementation(new ModelMongoRepositoryImpl()); :

@Bean
@Qualifier("one")
public ModelMongoRepository modelMongoRepositoryOne() throws DataAccessException, Exception {
    MongoRepositoryFactoryBean<ModelMongoRepository, Model, String> myFactory = new MongoRepositoryFactoryBean<ModelMongoRepository, Model, String>();
    MongoOperations mongoOperations = createMongoOperations("hostname1", 21979, "dbName1", "usdername1", "password1");
    myFactory.setCustomImplementation(new ModelMongoRepositoryImpl(mongoOperations));
    myFactory.setRepositoryInterface(ModelMongoRepository.class);
    myFactory.setMongoOperations(mongoOperations);

    myFactory.afterPropertiesSet();
    return myFactory.getObject();
}

If you were not wiring the repository manually through Java config, this implementation would HAVE to be named ModelMongoRepositoryImpl to match the interface ModelMongoRepository +"Impl". And it would be handled automatically by spring.

like image 76
alexbt Avatar answered Nov 10 '22 20:11

alexbt


For a general @Repository you can just add (value="someDao") to name the created Bean, if the MongoRepository extends Repository this should work.

like image 2
Gandalf Avatar answered Nov 10 '22 20:11

Gandalf