SpringBoot выбирает @Repository на основе шаблона проектирования и конфигурации

Небольшой вопрос о Spring Boot и о том, как использовать шаблон проектирования в сочетании с конфигурацией Spring @Value, чтобы выбрать соответствующий @Repository, пожалуйста.

Настройка: проект Springboot, который ничего не делает, кроме сохранения pojo. «Сложность» заключается в необходимости выбрать, где сохранить pojo, на основе некоторой информации из запроса полезной нагрузки.

Я начал с первой простой версии, которая выглядит так:

   @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";
        }
    }

С соответствующими @Configuration и @Repository для каждой базы данных. Я показываю 3 здесь, но представьте себе много. В проекте также есть способ внедрить будущие @Configuration и @Repository (на самом деле вопрос не здесь)

@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 {

Обратите внимание, что некоторые репозитории не являются дочерними элементами CrudRepository. Не существует прямого ___Репозитория, который может покрыть все.

И эта первая версия работает нормально. Очень счастлив, что означает, что я могу сохранить pojo там, где он должен быть сохранен, поскольку я получаю правильный компонент репозитория, используя эту структуру if else. На мой взгляд, эта структура не очень элегантна (если это нормально, если у нас здесь разные мнения), тем более, совсем не гибкая (нужно хардкодить каждый возможный репозиторий, опять же представьте себе множество).

Вот почему я пошел на рефакторинг и перешел на эту вторую версию:

@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();
    }

Как видите, я использую шаблон проектирования, преобразующий if-else в хэш-карту.

Кстати, этот пост не о if-else и hashmap.

Работает нормально, но обратите внимание, что это карта Map<String, Function<MyPojo, MyPojo>>, так как я не могу построить карту Map<String, @Repository>.

Во второй версии выполняется рефакторинг if-else, но опять же, нам нужно жестко закодировать хэш-карту.

Вот почему у меня возникла идея создать третью версию, в которой я могу настроить саму карту с помощью свойства весенней загрузки @Value for Map:

Вот что я пробовал:

@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();
    } 

И я бы настроил в файле свойств:

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

И завтра я смогу настроить добавление или удаление будущего целевого хранилища.

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

К сожалению, я застрял.

Каков правильный код, чтобы использовать настраиваемое свойство для сопоставления ключа с его «каким @Repository использовать», пожалуйста?

Спасибо за помощь.

В конце концов, версия if-else является наиболее читабельной.

Simon Martinelli 08.12.2022 16:31

но определенно не гибкий, так как все сопоставления репозиториев жестко закодированы в контроллере. (И хэш-карта довольно хороша! :))

PatPanda 08.12.2022 16:33

Что вы подразумеваете под гибким? Если вы получаете новый репозиторий, вам все равно придется прикасаться к коду.

Simon Martinelli 08.12.2022 16:34

Вы можете иметь интерфейс для представления всех репозиториев и использовать SPEL для использования того, который вам нужен, на основе свойств.

Leonardo Emmanuel de Azevedo 08.12.2022 16:59
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
4
249
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы можете создать базовый репозиторий, который будет расширен всеми вашими репозиториями:

public interface BaseRepository {
    MyPojo save(MyPojo onboarding);
}

поэтому у вас будет куча репозиториев, таких как:

@Repository("repoA")
public interface ARepository extends JpaRepository<MyPojo, String>, BaseRepository {
}

@Repository("repoB")
public interface BRepository extends JpaRepository<MyPojo, String>, BaseRepository {
}

...

Эти репозитории будут предоставлены фабрикой:

public interface BaseRepositoryFactory {
    BaseRepository getBaseRepository(String whereToSave);
}

которые вы должны настроить в ServiceLocatorFactoryBean:

@Bean
public ServiceLocatorFactoryBean baseRepositoryBean() {
    ServiceLocatorFactoryBean serviceLocatorFactoryBean = new ServiceLocatorFactoryBean();
    serviceLocatorFactoryBean.setServiceLocatorInterface(BaseRepositoryFactory.class);
    return serviceLocatorFactoryBean;
}

Теперь вы можете внедрить фабрику туда, куда вам нужно, и получить желаемое репо:

@Autowired
private BaseRepositoryFactory baseRepositoryFactory;

...

baseRepositoryFactory.getBaseRepository("repoA").save(myPojo);

...

Надеюсь, это поможет.

Привет @eltabo, спасибо за этот ответ. Это решение очень красивое, очень чистое. Использование Фабрики довольно разумно. Однако, поскольку репозитории будут расширять два интерфейса с помощью save(), существует риск reference to save is ambiguous, а также types my.repository.BaseRepository and org.springframework.data.repository.reactive.ReactiveCrudRep‌​ository<my.model.MyP‌​ojo,java.lang.String‌​> are incompatible;

PatPanda 16.12.2022 03:29

Кроме того, меня очень интересует карта @Value, поскольку она предлагает возможность настроить несколько сопоставлений «whereToSave» для одного и того же репозитория. Вместо @Repository("repoA") я могу сделать на карте {repoA:ARepository, mongo:MongoDbRepository , ещеAnotherWhereToWithValueA:ARepositry}

PatPanda 16.12.2022 03:33

С учетом сказанного, это решение очень продвинутое и приятное, проголосуйте за него.

PatPanda 16.12.2022 03:33

В этом случае я думаю, что предлагаемое вами решение имеет такую ​​​​же несовместимость с ReactiveCrudRepository, потому что вы полагаетесь на Function<MyPojo, MyPojo>, в любом случае давайте посмотрим

eltabo 17.12.2022 00:11

Вы пытались создать класс конфигурации для создания карты репозитория?

@Configuration
public class MyConfiguration {

  @Bean
  public Map repositoryMap() {
    Map<String, ? extends Repository> repositoryMap = new HashMap<>();
    
    repositoryMap.put('redis', new RedisRepository());
    repositoryMap.put('mongo', new MongoRepository());
    repositoryMap.put('elastic', new ElasticRepository());
    
    return Collections.unmodifiableMap(repositoryMap);
  
  }


}

Тогда у вас может быть следующее в вашем контроллере отдыха

@RestController
@Configuration
public class ControllerVersionFour {

    @Autowired
    private Map<String, ? extends Repository> repositoryMap;
    
    @PostMapping(path = "/save/{dbname}")
    public String save(@RequestBody MyRequest myRequest,  @PathVariable("dbname") String dbname) {
        MyPojo myPojo = new MyPojo(UUID.randomUUID().toString(), myRequest.getValue());
        return repisitoryMap.get(dbname).save(myPojo);
    }

Возможно, было бы лучше иметь базу данных в качестве параметра пути/запроса, а не в теле запроса. Таким образом, вы можете или не можете просто сохранить тело запроса в зависимости от вашего варианта использования вместо создания другого pojo.

Этот пост также может быть полезен для автопривязки карты

Привет @ mh377, это отличный пост! Спасибо! Однако наличие @Bean public Map repositoryMap() {, даже в отдельной @Configuration, по-прежнему требует, чтобы на уровне кода была жестко запрограммирована хэш-карта. Это намного лучшая версия моего ControllerVersionTwo, но все еще требует кода для хранения сопоставления. Что я ищу, так это конфигурацию из файла свойств для хранения конфигурации

PatPanda 17.12.2022 11:16

Затем, если вы хотите иметь его в конфигурации, например. сохраните имена классов в списке yaml, возможно, вы захотите создать новый экземпляр на основе этих имен классов, используя отражение stackoverflow.com/questions/6094575/…

mh377 17.12.2022 11:54

Если вы определите имя для каждого компонента @Repository, а затем @Autowired a Map<String, ? extends Repository>, возможно, вам даже не придется жестко кодировать карту. В документации говорится: «В случае типа зависимости массива, коллекции или карты контейнер автоматически связывает все компоненты, соответствующие объявленному типу значения». Может быть, это то, что вы ищете.

Raphallal 21.12.2022 17:25
Ответ принят как подходящий

Короткий ответ:

  • создать общий интерфейс
  • создайте несколько подклассов этого интерфейса (по одному на хранилище), используя разные имена компонентов Spring.
  • Используйте карту для работы с псевдонимами
  • используйте контекст Spring для получения правильного компонента по псевдониму (вместо создания пользовательской фабрики)

Теперь добавление нового хранилища — это только добавление новых Repository классов с именем

Объяснение: Как упоминалось в другом ответе, вам сначала нужно определить общий интерфейс, поскольку вы не можете использовать CrudRepository.save(...). В моем примере я повторно использую ту же подпись, что и метод save, чтобы избежать его повторной реализации в подклассах CrudRepository.

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

Репозиторий Redis:

@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;
    }
}

Для другого CrudRepository не нужно предоставлять реализацию:

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

Создайте конфигурацию для своих псевдонимов в application.yml

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

Создайте настраиваемые свойства для получения карты:

@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;
    }
}

Теперь создайте третью версию вашего репозитория с внедрением 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();
    }

}

Вы можете проверить, что это работает с тестом:

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);
    }

}

Привет @Philippe, это очень хороший ответ. Этот общий интерфейс + applicationContext.getBean() работает как шарм. Однако я все еще ищу карту, настроенную с помощью @Value. Потому что в вашем примере «whereToSave» может быть только «redis». Я надеюсь на конфигурацию стиля карты, где я могу сопоставить несколько ключей с RedisRepository, например, такая карта будет иметь два сопоставления ключей с репозиторием Redis: {redis:RedisRepository, mongo:MongoDbRepository , saveToRedisPlease:RedisRepositry}. С учетом сказанного, ваше решение очень хорошее. Проголосовать за

PatPanda 22.12.2022 13:42

Я меняю реализацию, чтобы получить карту, имеющую дело с псевдонимами.

Philippe K 22.12.2022 16:23

Это правильно, спасибо @Philippe! Спасибо за понятные объяснения

PatPanda 23.12.2022 01:42

Другие вопросы по теме