Небольшой вопрос о 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 использовать», пожалуйста?
Спасибо за помощь.
но определенно не гибкий, так как все сопоставления репозиториев жестко закодированы в контроллере. (И хэш-карта довольно хороша! :))
Что вы подразумеваете под гибким? Если вы получаете новый репозиторий, вам все равно придется прикасаться к коду.
Вы можете иметь интерфейс для представления всех репозиториев и использовать SPEL для использования того, который вам нужен, на основе свойств.




Вы можете создать базовый репозиторий, который будет расширен всеми вашими репозиториями:
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.ReactiveCrudRepository<my.model.MyPojo,java.lang.String> are incompatible;
Кроме того, меня очень интересует карта @Value, поскольку она предлагает возможность настроить несколько сопоставлений «whereToSave» для одного и того же репозитория. Вместо @Repository("repoA") я могу сделать на карте {repoA:ARepository, mongo:MongoDbRepository , ещеAnotherWhereToWithValueA:ARepositry}
С учетом сказанного, это решение очень продвинутое и приятное, проголосуйте за него.
В этом случае я думаю, что предлагаемое вами решение имеет такую же несовместимость с ReactiveCrudRepository, потому что вы полагаетесь на Function<MyPojo, MyPojo>, в любом случае давайте посмотрим
Вы пытались создать класс конфигурации для создания карты репозитория?
@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, но все еще требует кода для хранения сопоставления. Что я ищу, так это конфигурацию из файла свойств для хранения конфигурации
Затем, если вы хотите иметь его в конфигурации, например. сохраните имена классов в списке yaml, возможно, вы захотите создать новый экземпляр на основе этих имен классов, используя отражение stackoverflow.com/questions/6094575/…
Если вы определите имя для каждого компонента @Repository, а затем @Autowired a Map<String, ? extends Repository>, возможно, вам даже не придется жестко кодировать карту. В документации говорится: «В случае типа зависимости массива, коллекции или карты контейнер автоматически связывает все компоненты, соответствующие объявленному типу значения». Может быть, это то, что вы ищете.
Короткий ответ:
Теперь добавление нового хранилища — это только добавление новых 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}. С учетом сказанного, ваше решение очень хорошее. Проголосовать за
Я меняю реализацию, чтобы получить карту, имеющую дело с псевдонимами.
Это правильно, спасибо @Philippe! Спасибо за понятные объяснения
В конце концов, версия if-else является наиболее читабельной.