Search code examples
javaspring-bootdesign-patternsrepositoryrepository-pattern

SpringBoot selecting the @Repository based on design pattern and configuration


Small question on Spring Boot, and how to use a design pattern combined with Spring @Value configuration in order to select the appropriate @Repository please.

Setup: A springboot project which does nothing but save a pojo. The "difficulty" is the need to choose where to save the pojo, based on some info from inside the payload request.

I started with a first straightforward version, which looks like this:

   @RestController
public class ControllerVersionOne {

    @Autowired private ElasticRepository elasticRepository;
    @Autowired private MongoDbRepository mongoRepository;
    @Autowired private RedisRepository redisRepository;

    //imagine many more other repositories
//imagine many more other repositories
//imagine many more other repositories

    @PostMapping(path = "/save")
    public String save(@RequestBody MyRequest myRequest) {
        String whereToSave = myRequest.getWhereToSave();
        MyPojo myPojo = new MyPojo(UUID.randomUUID().toString(), myRequest.getValue());
        if (whereToSave.equals("elastic")) {
            return elasticRepository.save(myPojo).toString();
        } else if (whereToSave.equals("mongo")) {
            return mongoRepository.save(myPojo).toString();
        } else if (whereToSave.equals("redis")) {
            return redisRepository.save(myPojo).toString();
            // imagine many more if 
            // imagine many more if 
            // imagine many more if 

        } else {
            return "unknown destination";
        }
    }

With the appropriate @Configuration and @Repository for each and every databases. I am showing 3 here, but imagine many. The project has a way to inject future @Configuration and @Repository as well (the question is not here actually)

@Configuration
public class ElasticConfiguration extends ElasticsearchConfiguration {

@Repository
public interface ElasticRepository extends CrudRepository<MyPojo, String> {


@Configuration
public class MongoConfiguration extends AbstractMongoClientConfiguration {

@Repository
public interface MongoDbRepository extends MongoRepository<MyPojo, String> {


@Configuration
public class RedisConfiguration {

@Repository
public interface RedisRepository {

Please note, some of the repositories are not children of CrudRepository. There is no direct ___Repository which can cover everything.

And this first version is working fine. Very happy, meaning I am able to save the pojo to where it should be saved, as I am getting the correct repository bean, using this if else structure. In my opinion, this structure is not very elegant (if it ok if we have different opinion here), especially, not flexible at all (need to hardcode each and every possible repository, again imagine many).

This is why I went to refactor and change to this second version:

@RestController
public class ControllerVersionTwo {

    private ElasticRepository elasticRepository;
    private MongoDbRepository mongoRepository;
    private RedisRepository redisRepository;
    private Map<String, Function<MyPojo, MyPojo>> designPattern;

    @Autowired
    public ControllerVersionTwo(ElasticRepository elasticRepository, MongoDbRepository mongoRepository, RedisRepository redisRepository) {
        this.elasticRepository = elasticRepository;
        this.mongoRepository = mongoRepository;
        this.redisRepository = redisRepository;
// many more repositories
        designPattern = new HashMap<>();
        designPattern.put("elastic", myPojo -> elasticRepository.save(myPojo));
        designPattern.put("mongo", myPojo -> mongoRepository.save(myPojo));
        designPattern.put("redis", myPojo -> redisRepository.save(myPojo));
//many more put
    }

    @PostMapping(path = "/save")
    public String save(@RequestBody MyRequest myRequest) {
        String whereToSave = myRequest.getWhereToSave();
        MyPojo myPojo = new MyPojo(UUID.randomUUID().toString(), myRequest.getValue());
        return designPattern.get(whereToSave).apply(myPojo).toString();
    }

As you can see, I am leveraging a design pattern refactoring the if-else into a hashmap.

This post is not about if-else vs hashmap by the way.

Working fine, but please note, the map is a Map<String, Function<MyPojo, MyPojo>>, as I cannot construct a map of Map<String, @Repository>.

With this second version, the if-else is being refactored, but again, we need to hardcode the hashmap.

This is why I am having the idea to build a third version, where I can configure the map itself, via a spring boot property @Value for Map:

Here is what I tried:

@RestController
public class ControllerVersionThree {

    @Value("#{${configuration.design.pattern.map}}")
    Map<String, String> configurationDesignPatternMap;

    private Map<String, Function<MyPojo, MyPojo>> designPatternStrategy;

    public ControllerVersionThree() {
        convertConfigurationDesignPatternMapToDesignPatternStrategy(configurationDesignPatternMap, designPatternStrategy);
    }

    private void convertConfigurationDesignPatternMapToDesignPatternStrategy(Map<String, String> configurationDesignPatternMap, Map<String, Function<MyPojo, MyPojo>> designPatternStrategy) {
        // convert configurationDesignPatternMap
        // {elastic:ElasticRepository, mongo:MongoDbRepository , redis:RedisRepository , ...}
        // to a map where I can directly get the appropriate repository based on the key
    }

    @PostMapping(path = "/save")
    public String save(@RequestBody MyRequest myRequest) {
        String whereToSave = myRequest.getWhereToSave();
        MyPojo myPojo = new MyPojo(UUID.randomUUID().toString(), myRequest.getValue());
        return designPatternStrategy.get(whereToSave).apply(myPojo).toString();
    } 

And I would configure in the property file:

configuration.design.pattern.map={elastic:ElasticRepository, mongo:MongoDbRepository , saveToRedis:RedisRepositry, redis:RedisRepository , ...}

And tomorrow, I would be able to configure add or remove the future repository target.

configuration.design.pattern.map={elastic:ElasticRepository, anotherElasticKeyForSameElasticRepository, redis:RedisRepository , postgre:PostGreRepository}

Unfortunately, I am stuck.

What is the correct code in order to leverage a configurable property for mapping a key with it's "which @Repository to use" please?

Thank you for your help.


Solution

  • Short answer:

    • create a shared interface
    • create multiple sub-class of this interface (one per storage) using different spring component names
    • Use a map to deal with aliases
    • use Spring context to retrieve the right bean by alias (instead of creating a custom factory)

    Now adding a new storage is only adding a new Repository classes with a name

    Explanation: As mentioned in the other answer you first need to define a common interface as you can't use the CrudRepository.save(...). In my example I reuse the same signature as the save method to avoid re-implementing it in the sub-classes of CrudRepository.

    public interface MyInterface<T> {
        <S extends T> S save(S entity);
    }
    

    Redis Repository:

    @Repository("redis") // Here is the name of the redis repo
    public class RedisRepository implements MyInterface<MyPojo>  {
        @Override
        public <S extends MyPojo> S save(S entity) {
            entity.setValue(entity.getValue() + " saved by redis");
            return entity;
        }
    }
    

    For the other CrudRepository no need to provide an implementation:

    @Repository("elastic") // Here is the name of the elastic repo
    public interface ElasticRepository  extends CrudRepository<MyPojo, String>, MyInterface<MyPojo> {
    }
    

    Create a configuration for your aliases in application.yml

    configuration:
      design:
        pattern:
          map:
            redis: redis
            saveToRedisPlease: redis
            elastic: elastic
    

    Create a custom properties to retrieve the map:

    @Component
    @ConfigurationProperties(prefix = "configuration.design.pattern")
    public class PatternProperties {
        private Map<String, String>  map;
    
        public String getRepoName(String alias) {
            return map.get(alias);
        }
    
        public Map<String, String> getMap() {
            return map;
        }
    
        public void setMap(Map<String, String> map) {
            this.map = map;
        }
    }
    

    Now create the version three of your repository with the injection of SpringContext:

    @RestController
    public class ControllerVersionThree {
    
        private final ApplicationContext context;
    
        private PatternProperties designPatternMap;
    
        public ControllerVersionThree(ApplicationContext context,
                                      PatternProperties designPatternMap) {
            this.context = context;
            this.designPatternMap = designPatternMap;
        }
    
        @PostMapping(path = "/save")
        public String save(@RequestBody MyRequest myRequest) {
            String whereToSave = myRequest.getWhereToSave();
            MyPojo myPojo = new MyPojo(UUID.randomUUID().toString(), myRequest.getValue());
            String repoName = designPatternMap.getRepoName(whereToSave);
            MyInterface<MyPojo> repo = context.getBean(repoName, MyInterface.class);
            return repo.save(myPojo).toString();
        }
    
    }
    

    You can check that this is working with a test:

    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.web.client.TestRestTemplate;
    import org.springframework.boot.test.web.server.LocalServerPort;
    import org.springframework.http.HttpEntity;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    class ControllerVersionThreeTest {
        @LocalServerPort
        private int port;
        @Autowired
        private TestRestTemplate restTemplate;
    
        @Test
        void testSaveByRedis() {
            // Given: here 'redis' is the name of the spring beans
            HttpEntity<MyRequest> request = new HttpEntity<>(new MyRequest("redis", "aValue"));
    
            // When
            String response = restTemplate.postForObject("http://localhost:" + port + "/save", request, String.class);
    
            // Then
            assertEquals("MyPojo{value='aValue saved by redis'}", response);
        }
    
        @Test
        void testSaveByRedisAlias() {
            // Given: here 'saveToRedisPlease' is an alias name of the spring beans
            HttpEntity<MyRequest> request = new HttpEntity<>(new MyRequest("saveToRedisPlease", "aValue"));
    
            // When
            String response = restTemplate.postForObject("http://localhost:" + port + "/save", request, String.class);
    
            // Then
            assertEquals("MyPojo{value='aValue saved by redis'}", response);
        }
    
    }