Как получить отдельный элемент по ключу из кеша в кеше Spring в Spring Boot?

Мы добавили spring-boot-starter-cache в наш проект и не используем какую-либо конкретную реализацию поставщика кеша. мы загружаем все данные во время запуска приложения, вызывая следующий метод:

@Override
@Cacheable(cacheNames = "foos")
public List<FooDto> getAllFoo() {
    return fooRepository.findAll().stream()
            .map(FooEntityDomainToDtoMapper::mapDomainToDto) // mapping entity to dto
            .collect(Collectors.toList());
}

//Want to implement something like:
    public FooDto getFoo(Long id) {
    //return single object from foos(which are cached in above method)
    }

Он хранит все foos в кеше. И, как ожидается, в следующий раз, когда мы будем вызывать getAllFoo, он будет возвращаться из кеша, а не из базы данных. Теперь в следующий раз, когда пользователь запрашивает отдельный объект по идентификатору, мы хотим вернуть его из этих уже кэшированных foos данных, а не вызывать findById() из JPA. Есть ли способ добиться этого?


person implosivesilence    schedule 28.09.2020    source источник
comment
Вы можете получить доступ к CacheManager напрямую. Проблема для вас в том, что вам нужно автоматически подключить его, и вы не можете сделать это в интерфейсе, потому что для JPA Spring генерирует реализацию репо во время выполнения. Итак, вам нужен некоторый компонент-оболочка, который делегирует ваш @Repository, где вы можете автоматически подключить диспетчер кеша и при необходимости пропустить вызов JPA.   -  person Michael    schedule 28.09.2020


Ответы (2)


Есть ли какая-либо причина, по которой вы хотите или должны кэшировать все Foos в своем приложении вместе, а не по отдельности?

Имейте в виду, что по замыслу Spring's Cache Abstraction параметры метода (если есть) используются в качестве ключа, а возвращаемое значение — в качестве значения записи кэша. Если у метода нет параметров, то Spring сгенерирует для вас идентификатор.

Я написал о том, как настроить Spring CacheManager для кэширования коллекции значений, возвращаемых методом @Cacheable, по отдельности.

Однако на данный момент предположим, что вам нужно/хотите кэшировать весь список Foos.

Затем, чтобы создать метод, который извлекает отдельный Foo по идентификатору из кэшированного списка Foos, вы могли бы, учитывая исходный кэшированный метод в классе обслуживания, сделать, например...

@Sevice
class MyFooService {

  private final FooRepository<Foo, Long> fooRepository;

  @Cacheable(cacheNames = "foos")
  public List<FooDto> getAllFoos() {
    return this.fooRepository.findAll().stream()
      .map(FooEntityDomainToDtoMapper::mapDomainToDto) // mapping entity to dto
      .collect(Collectors.toList());
  }
}

Затем в другом компоненте приложения вы можете...

@Component
class MyFooAccessor {

  private final MyFooService fooService;

  MyFooAccessor(MyFooService fooService) {
    this.fooService = fooService;
  }

  Optional<FooDto> getById(Long id) {
    this.fooService.getAllFoos().stream()
      .filter(fooDto -> fooDto.getId().equals(id))
      .findFirst();
  }

  ...

}

MyFooAccessor следит за тем, чтобы вы не обходили прокси-сервер кэширования (т. е. AOP Proxy + Caching Advice вокруг MyFooService, применяемый Spring). Если бы метод getById(..) был членом класса MyFooService и вызывал бы метод getAllFoos() напрямую, вы бы обходили рекомендации по прокси и кэшированию, что каждый раз приводило бы к доступу к базе данных.

ПРИМЕЧАНИЕ. Вы можете использовать Spring AOP Load Time Weaving (LTW) (см. doc), чтобы избежать обхода кэширующего прокси-сервера, если вы хотите сохранить метод getById(:Long) в классе MyFooService с getAllFoos() , @Cacheable метод. Однако...

Как правило, вы можете решить проблемы такого рода путем (ре)структурирования вашего кода соответствующим образом, используя правильный шаблон проектирования. И это не единственное решение. Прелесть Spring заключается в том, что он дает вам много вариантов выбора. Это всего лишь 1 вариант.

Надеюсь, это поможет вам получить больше идей.

person John Blum    schedule 28.09.2020
comment
Причина, по которой я задаю первый вопрос, заключается в том, что кэширование всех элементов может стать довольно дорогим. Помните, что кеш обычно находится в памяти для большинства систем кэширования (конечно, с политиками переполнения, истечения срока действия, вытеснения и даже сохранения, в зависимости от поставщика кэширования), но чем больше вы держите в памяти, тем больше вероятность давления GC и паузы. Просто будь осторожен. - person John Blum; 28.09.2020
comment
В большинстве случаев лучше кэшировать часто используемые элементы по мере необходимости, лениво. Используя эвристику, логическую дедукцию или простые отношения, можно также предварительно кэшировать другие объекты, которые, как вы знаете, будут доступны пользователю в следующий раз на основе объекта, к которому в настоящее время осуществляется доступ, если рабочий процесс достаточно сложен. В любом случае, вы будете знать, что делать. - person John Blum; 28.09.2020
comment
Спасибо, @john-blum. Причина в том, что мы хотим загрузить все данные, используя запуск системы, используя что-то вроде PostConstruct, а затем хотим получить отдельные объекты, используя ключ из этих кэшированных объектов. Я думал об использовании потока в списке, но каждый раз, когда пользователь запрашивает один объект, мне приходится повторять весь поток. Итак, я вернул ‹Long, Foo› в методы getAllFoos(), преобразовав список, возвращаемый fooRepository.findAll(), в карту. Мне было интересно, есть ли в Spring какой-то готовый (или собственный) метод для чтения из набора кэшированных данных (например, «foos» в этом случае) с использованием некоторого ключа/идентификатора. - person implosivesilence; 29.09.2020
comment
Учитывая это описание, я, вероятно, подошел бы к этому по-другому. Похоже, вы пытаетесь предварительно предупредить кеш. Если да, то вы можете сделать 1 из 2 вещей. 1) Вставьте CacheManager (или извлеките его из ApplicationContext в ApplicationListener, слушая ContextRefreshedEvent как Michael, как описано выше), получите соответствующий Cache по имени (например, FoosById) и используйте Repository для загрузки кеша. . Затем просто укажите @Cacheable("FoosById") FooDto getByFooId(Long id). Или... - person John Blum; 29.09.2020
comment
2) Вместо того, чтобы внедрять или искать CacheManager, просто добавьте метод @CachePut("FoosById") save(:FooDto) в управляемый класс MyFoosService в ApplicationListener и вызовите этот метод для каждого FooDto, возвращаемого методом Repository.findAll() внутри прослушивателя. Позвольте мне продемонстрировать ... - person John Blum; 29.09.2020
comment
А вот пример/тестовый класс, демонстрирующий 2 подхода, которые я описал выше в комментариях... github.com/jxblum/stackoverflow-questions-answers/blob/master/ - person John Blum; 30.09.2020
comment
Вы можете переключаться между двумя подходами (1 и 2), изменяя Spring Profile, используемый тестовым классом, но я бы предпочел PreWarmCacheUsingService в вашем случае использования, если это был я. Надеюсь, это все поможет. - person John Blum; 30.09.2020
comment
Спасибо, @john-blum. Если я хочу сохранить метод getById(..) в том же MyFooService, то должен использовать Load Time Weaving (LTW), не могли бы вы помочь с этим? например как? Каковы недостатки этого подхода? - person implosivesilence; 30.09.2020
comment
Кроме того, мы не можем сохранить в бэкэнд, используя «XRepository.save()», так как это данные поиска, которые уже присутствуют в базе данных. - person implosivesilence; 30.09.2020
comment
Если бы я продолжил использовать «MyFooAccessor», какой шаблон проектирования мы должны использовать? - person implosivesilence; 30.09.2020
comment
О LTW см. здесь... docs.spring.io/spring-framework/docs/current/. Никаких недостатков (обязательно), только компромиссы. Имейте в виду, что LTW изменяет байт-код ваших управляемых компонентов во время выполнения. AspectJ делает это с помощью агента Java, но Spring может сделать это с помощью дополнительного JAR-файла в пути к классам приложения. - person John Blum; 30.09.2020
comment
По поводу Repository.save(..)... точно! Вот почему вместо этого я реализовал метод cache(..) (github.com/jxblum/stackoverflow-questions-answers/blob/master/). Метод cache(..) используется для предварительной загрузки кеша в слушателе (github.com/jxblum/stackoverflow-questions-answers/blob/master/). - person John Blum; 30.09.2020
comment
С MyFooAccessor это позволяет избежать LTW. Но это требует, чтобы вы тщательно структурировали свой код таким образом, чтобы вы всегда обращались к ПРОКСИ @Cacheable FooService для вызова логики кэширования (т.е. AOP Advice). На самом деле нет правильного или неправильного подхода к этому. Это просто предпочтение. Выберите то, что лучше всего подходит для вашей унифицированной коммуникации, требований и т. д., и, что более важно, для вашего понимания с точки зрения обслуживания. Простота всегда лучше, чем ум, ИМО. - person John Blum; 30.09.2020

Кэшируйте объект с помощью ключа, чтобы при извлечении из кеша вы могли использовать этот ключ.

@Override
@Cacheable(value = "fooByIDCache", key = "#id", unless = "#result == null")
public FooDto getFooByID(String id, FooDto fooDTO) {
    return fooDTO;
}
person Vaibs    schedule 29.09.2020
comment
для выборки по идентификатору мы не хотим давать отдельный вызов базы данных. мы получили все значения с помощью метода findAll() и для последующего запроса по идентификатору хотим вернуться из уже кэшированных объектов. как вы возвращаете значения из кеша в вышеуказанном методе? - person implosivesilence; 29.09.2020
comment
Список возврата вашего метода, вы должны повторить этот метод и отфильтровать по идентификатору и получить элемент. Мой ответ для кэширования каждого элемента по идентификатору. - person Vaibs; 29.09.2020
comment
Хотя это был бы предпочтительный подход (ленивое кэширование отдельных Foos, как я упоминал в своем ответе выше), если есть необходимость в кэшировании всех Foos (т. е. в методе getAllFoos()), то вы бы дважды кэшировали объекты... 1 запись для каждого Foo по идентификатору и вторая запись для List при запуске. - person John Blum; 29.09.2020