Тема . Обработка ошибок в контроллерах Spring - BelyiZ/JavaCourses GitHub Wiki

Материал находится на стадии "Черновик"

Содержание:

  1. Особенности запуска программы
  2. Сложные запросы к БД
  3. Обзор возможностей Spring Cache
  4. Список литературы/курсов

Обзор вариантов кеширования

JDBC API предоставляет программный доступ к реляционным базам данных из программ, написанных на Java. Так же рассказывает о том, что JDBC API является частью платформы Java и входит поэтому в Java SE и Java EE. JDBC API представлен двумя пакетами: java.sql and javax.sql.

Сложные запросы к БД

Запросы к базе данных делаются с помощью Spring JDBC.

Предварительная подготовка Из зависимостей нам нужны data-jdbc — стартер, flyway для управления схемой и драйвер postgres для подключения к базе данных.

// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'

    implementation 'org.flywaydb:flyway-core'

    runtimeOnly 'org.postgresql:postgresql'
}
        

Далее настраиваем приложение для подключения к базе данных:

# application.yml
spring:
application:
name: template-app
datasource:
url: jdbc:postgresql://localhost:5432/demo_app?currentSchema=app
username: app_user
password: change_me
driver-class-name: org.postgresql.Driver

Маппинг сущностей Для этого примера будем использовать следующую таблицу:

create table book (
id varchar(32) not null,
title varchar(255) not null,
author varchar(255),
isbn varchar(15),
published_date date,
page_count integer,
primary key (id)
);

И соответствующий java-класс (обратите внимание, что @Id импортируется из org.springframework.data.annotation.Id):

// Book.java
public class Book {
@Id
private String id;
private String title;
private String author;
private String isbn;
private Instant publishedDate;
private Integer pageCount;
}

Однако, если мы запустим тест:

// BookRepositoryTest.java
@Test
void canSaveBook() {
var book = Book.builder().author("Steven Erikson").title("Gardens of the Moon").build();
var savedBook = bookRepository.save(book);

    assertThat(savedBook.getId()).isNotBlank();
    assertThat(savedBook.getAuthor()).isEqualTo(book.getAuthor());
    assertThat(savedBook.getTitle()).isEqualTo(book.getTitle());

    assertThat(savedBook).isEqualTo(bookRepository.findById(savedBook.getId()).get());
}

То увидим ошибку — ERROR: null value in column "id" violates not-null constraint. Это происходит, потому что не определен ни способ генерации id ни значение по умолчанию. Поведение Spring Data JDBC в части идентификаторов немного отличается от Spring Data JPA. В примере нужно определить ApplicationListener для BeforeSaveEvent:

// PersistenceConfig.java
@Bean
public ApplicationListener<BeforeSaveEvent> idGenerator() {
return event -> {
var entity = event.getEntity();
if (entity instanceof Book) {
((Book) entity).setId(UUID.randomUUID().toString());
}
};
}

Теперь тест пройдет, потому что поле Id заполняется. Полный список поддерживаемых событий жизненного цикла смотрите в документации.

Методы запросов Одной из особенностей проектов Spring Data является возможность определять методы запросов в репозиториях. Spring Data JDBC использует здесь несколько иной подход. Для демонстрации определим метод запроса в BookRepository:

Optional<Book> findByTitle(String title);

И если запустим соответствующий тест:

@Test
void canFindBookByTitle() {
var title = "Gardens of the Moon";
var book = Book.builder().author("Steven Erikson").title(title).build();
var savedBook = bookRepository.save(book);
assertThat(bookRepository.findByTitle(title).get()).isEqualTo(savedBook);
}

Получим ошибку —

java Caused by: java.lang.IllegalStateException: No query specified on findByTitle. 

В настоящее время Spring Data JDBC поддерживает только явные запросы, задаваемые через @Query.

Напишем sql-запрос для нашего метода:

@Query("select * from Book b where b.title = :title")
Optional<Book> findByTitle(@Param("title") String title);

Тест пройден!

Связи Для работы со связями Spring Data JDBC также использует другой подход. Основное отличие в том, что отсутствует ленивая загрузка. Поэтому если не нужна связь в сущности, то просто не добавляйте ее туда. Такой подход основан на одной из концепций предметно-ориентированного проектирования (Domain Driven Design), согласно которой сущности, которые мы загружаем, являются корнями агрегатов, поэтому проектировать надо так, чтобы корни агрегатов тянули за собой загрузку других классов.

Один-к-одному Для связей "один-к-одному" и "один-ко-многим" используется аннотация @MappedCollection. Сначала посмотрим на "один-к-одному". Класс UserAccount будет ссылаться на Address. Вот соответствующий sql:

create table address
(
id      varchar(36) not null,
city    varchar(255),
state   varchar(255),
street  varchar(255),
zipcode varchar(255),
primary key (id)
);

create table user_account
(
id         varchar(36)  not null,
name       varchar(255) not null,
email      varchar(255) not null,
address_id varchar(36),
primary key (id),
constraint fk_user_account_address_id foreign key (address_id) references address (id)
);

Класс UserAccount выглядит примерно так:

// UserAccount.java
public class UserAccount implements GeneratedId {
// ...other fields
@MappedCollection(idColumn = "id")
private Address address;
}

Здесь опущены другие поля, чтобы показать маппинг address. Значение в idColumn — это имя поля идентификатора класса Address. Обратите внимание, что в классе Address нет ссылки на класс UserAccount, поскольку агрегатом является UserAccount. Это продемонстрировано в тесте:

//UserAccountRepositoryTest.java
@Test
void canSaveUserWithAddress() {
var address = stubAddress();
var newUser = stubUser(address);

    var savedUser = userAccountRepository.save(newUser);

    assertThat(savedUser.getId()).isNotBlank();
    assertThat(savedUser.getAddress().getId()).isNotBlank();

    var foundUser = userAccountRepository.findById(savedUser.getId()).orElseThrow(IllegalStateException::new);
    var foundAddress = addressRepository.findById(foundUser.getAddress().getId()).orElseThrow(IllegalStateException::new);

    assertThat(foundUser).isEqualTo(savedUser);
    assertThat(foundAddress).isEqualTo(savedUser.getAddress());
}

Один-ко-многим Вот sql, который будем использовать для демонстрации связи "один-ко-многим":

create table warehouse
(
id       varchar(36) not null,
location varchar(255),
primary key (id)
);

create table inventory_item
(
id        varchar(36) not null,
name      varchar(255),
count     integer,
warehouse varchar(36),
primary key (id),
constraint fk_inventory_item_warehouse_id foreign key (warehouse) references warehouse (id)
);

В этом примере на складе (warehouse) есть много товаров/объектов (inventoryitems). Поэтому в классе Warehouse мы также будем использовать @MappedCollection для InventoryItem:

public class Warehouse {
// ...other fields
@MappedCollection
Set<InventoryItem> inventoryItems = new HashSet<>();

    public void addInventoryItem(InventoryItem inventoryItem) {
        var itemWithId = inventoryItem.toBuilder().id(UUID.randomUUID().toString()).build();
        this.inventoryItems.add(itemWithId);
    }
}

public class InventoryItem {
@Id
private String id;
private String name;
private int count;
}

В этом примере мы устанавливаем поле id во вспомогательном методе addInventoryItem. Можно также определить ApplicationListener для класса Warehouse с обработкой BeforeSaveEvent, в котором установить поле id для всех InventoryItem. Вам не обязательно делать в точности так, как сделано у меня. Посмотрите тесты с демонстрацией некоторых особенностей поведения связи "один-ко-многим". Главное то, что сохранение или удаление экземпляра Warehouse влияет на соответствующие InventoryItem.

В нашем случае InventoryItem не должен знать о Warehouse. Таким образом, у этого класса есть только те поля, которые описывают его. В JPA принято делать двусторонние связи, но это может быть громоздким и провоцировать ошибки, если забудете поддерживать обе стороны связи. Spring Data JDBC способствует созданию только необходимых вам связей, поэтому обратная связь "многие-к-одному" здесь не используется.

Многие-к-одному и многие-ко-многим В рамках этого руководства я не буду вдаваться в подробности о связях "многие-к-одному" или "многие ко многим". Я советую избегать связей "многие-ко-многим" и использовать их только в крайнем случае. Хотя иногда они могут быть неизбежны. Оба этих типа связей реализуются в Spring Data JDBC через ссылки на Id связанных сущностей. Поэтому имейте ввиду, что здесь вам предстоит еще немного потрудиться.

Итог:

Spring Data JDBC стремится быть проще, и поэтому отсутствует ленивая загрузка. Помимо этого, отсутствует кеширование, отслеживание "грязных" объектов (dirty tracking) и сессии (session). Если в Spring Data JDBC загружаете объект, то он загружается полностью (включая связи) и сохраняется тогда, когда сохраняете его в репозиторий.

Обзор возможностей Spring Cache

Для наглядности продемонстрируем

0. Создание проекта

Создадим простой проект, в котором мы сможем брать сущность из базы данных. Добавим в проект Lombok, Spring Cache, Spring Data JPAи H2. Хотя, вполне можно обойтись только Spring Cache.

plugins {
id 'org.springframework.boot' version '2.1.7.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
}

group = 'ru.xpendence'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

У нас будет только одна сущность, назовём её User.

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@ToString
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "email")
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

Добавим репозиторий и сервис:

public interface UserRepository extends JpaRepository<User, Long> {
}

@Slf4j
@Service
public class UserServiceImpl implements UserService {

    private final UserRepository repository;

    public UserServiceImpl(UserRepository repository) {
        this.repository = repository;
    }

    @Override
    public User create(User user) {
        return repository.save(user);
    }

    @Override
    public User get(Long id) {
        log.info("getting user by id: {}", id);
        return repository.findById(id)
                .orElseThrow(() -> new EntityNotFoundException("User not found by id " + id));
    }
}

Когда заходим в сервисный метод get(), пишем об этом в лог.

Подключим к проекту Spring Cache.

@SpringBootApplication
@EnableCaching    //подключение Spring Cache
public class CacheApplication {

    public static void main(String[] args) {
        SpringApplication.run(CacheApplication.class, args);
    }

}

Проект готов.

1. Кэширование возвращаемого результата

Функция Spring Cache - кэшировать возвращаемый результат для определённых входных параметров. Далее проверим данный факт. Поставим аннотацию @Cacheable над сервисным методом get(), чтобы кэшировать возвращаемые данные. Дадим этой аннотации название "users".

    @Override
    @Cacheable("users")
    public User get(Long id) {
        log.info("getting user by id: {}", id);
        return repository.findById(id)
                .orElseThrow(() -> new EntityNotFoundException("User not found by id " + id));
    }

Для того, чтобы проверить, как это работает, напишем простой тест.

@RunWith(SpringRunner.class)
@SpringBootTest
public abstract class AbstractTest {
}

@Slf4j
public class UserServiceTest extends AbstractTest {

    @Autowired
    private UserService service;

    @Test
    public void get() {
        User user1 = service.create(new User("Vasya", "[email protected]"));
        User user2 = service.create(new User("Kolya", "[email protected]"));

        getAndPrint(user1.getId());
        getAndPrint(user2.getId());
        getAndPrint(user1.getId());
        getAndPrint(user2.getId());
    }

    private void getAndPrint(Long id) {
        log.info("user found: {}", service.get(id));
    }
}

Тест создаёт двоих пользователей и потом по 2 раза вытаскивает их из базы. Как мы помним, мы поместили аннотацию @Cacheable, которая будет кэшировать возвращаемые значения. После получения объекта из метода get() выводим объект в лог. Также, выводим в лог информацию о каждом посещении приложением метода get().

Запустим тест. Результат, передаваемый в консоль:

getting user by id: 1
user found: User(id=1, name=Vasya, email=vasya@mail.ru)
getting user by id: 2
user found: User(id=2, name=Kolya, email=kolya@mail.ru)
user found: User(id=1, name=Vasya, email=vasya@mail.ru)
user found: User(id=2, name=Kolya, email=kolya@mail.ru)

Первые два раза действительно сходили в метод get() и реально получили пользователя из базы. Во всех остальных случаях, реального захода в метод не было, приложение брало закэшированные данные по ключу (в данном случае, это id).

2. Объявление ключа для кэширования

Бывают ситуации, когда в кэшируемый метод приходит несколько параметров. В таком случае, бывает нужно определить параметр, по которому будет происходить кэширование. Добавим в пример метод, который будет сохранять в базу сущность, собранную по параметрам, но если сущность с таким именем уже есть, не будем её сохранять. Для этого определим параметр name как ключ для кэширования.

Выглядеть это будет так:

    @Override
    @Cacheable(value = "users", key = "#name")
    public User create(String name, String email) {
        log.info("creating user with parameters: {}, {}", name, email);
        return repository.save(new User(name, email));
    }

Напишем соответствующий тест:

    @Test
    public void create() {
        createAndPrint("Ivan", "[email protected]");
        createAndPrint("Ivan", "[email protected]");
        createAndPrint("Sergey", "[email protected]");

        log.info("all entries are below:");
        service.getAll().forEach(u -> log.info("{}", u.toString()));
    }

    private void createAndPrint(String name, String email) {
        log.info("created user: {}", service.create(name, email));
    }

Попробуем создать троих пользователей, для двоих из которых будет совпадать имя

        createAndPrint("Ivan", "[email protected]");
        createAndPrint("Ivan", "[email protected]");

и для двоих из которых будет совпадать email

        createAndPrint("Ivan", "[email protected]");
        createAndPrint("Sergey", "[email protected]");

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

Результат будет таким:

creating user with parameters: Ivan, ivan@mail.ru
created user: User(id=1, name=Ivan, email=ivan@mail.ru)
created user: User(id=1, name=Ivan, email=ivan@mail.ru)
creating user with parameters: Sergey, ivan@mail.ru
created user: User(id=2, name=Sergey, email=ivan@mail.ru)
all entries are below:
User(id=1, name=Ivan, email=ivan@mail.ru)
User(id=2, name=Sergey, email=ivan@mail.ru)

Фактически приложение вызывалось метод 3 раза, а заходило в него только два раза. Один раз для метода совпадал ключ, и он просто возвращал закэшированное значение.

3. Принудительное кэширование. @CachePut

Бывают ситуации, когда необходимо кэшировать возвращаемое значение для какой-то сущности, но в то же время, нужно обновить кэш. Для таких нужд существует аннотация @CachePut. Оно пропускает приложение в метод, при этом, обновляя кэш для возвращаемого значения, даже если оно уже закэшировано.

Добавим пару методов, в которых мы будем сохранять юзера. Один из них мы пометим обычной аннотацией @Cacheable, второй — @CachePut.

    @Override
    @Cacheable(value = "users", key = "#user.name")
    public User createOrReturnCached(User user) {
        log.info("creating user: {}", user);
        return repository.save(user);
    }

    @Override
    @CachePut(value = "users", key = "#user.name")
    public User createAndRefreshCache(User user) {
        log.info("creating user: {}", user);
        return repository.save(user);
    }

Первый метод будет просто возвращать закэшированные значения, второй — принудительно обновлять кэш. Кэширование будет осуществляться по ключу #user.name.

Напишем соответствующий тест.

    @Test
    public void createAndRefresh() {
        User user1 = service.createOrReturnCached(new User("Vasya", "[email protected]"));
        log.info("created user1: {}", user1);

        User user2 = service.createOrReturnCached(new User("Vasya", "[email protected]"));
        log.info("created user2: {}", user2);

        User user3 = service.createAndRefreshCache(new User("Vasya", "[email protected]"));
        log.info("created user3: {}", user3);

        User user4 = service.createOrReturnCached(new User("Vasya", "[email protected]"));
        log.info("created user4: {}", user4);
    }

По той логике, которая уже описывалась, при первом сохранении пользователя с именем "Vasya" через метод createOrReturnCached() далее будем получать кэшированную сущность, при этом, в сам метод приложение заходить не будет. Если же вызовем метод createAndRefreshCache(), кэшированная сущность для ключа с именем "Vasya" перезапишется в кэше. Выполним тест и посмотрим, что будет выведено в консоль.

creating user: User(id=null, name=Vasya, email=vasya@mail.ru)
created user1: User(id=1, name=Vasya, email=vasya@mail.ru)
created user2: User(id=1, name=Vasya, email=vasya@mail.ru)
creating user: User(id=null, name=Vasya, email=kolya@mail.ru)
created user3: User(id=2, name=Vasya, email=kolya@mail.ru)
created user4: User(id=2, name=Vasya, email=kolya@mail.ru)

Мы видим, что "user1" благополучно записался в базу и кэш. При повторной попытке записать юзера с таким же именем мы получаем закэшированный результат выполнения первого обращения ("user2", для которого id такой же, как у user1, что говорит нам о том, что юзер не был записан, и это просто кэш). Далее, мы пишем третьего пользователя через второй метод, который даже при имеющемся закэшированном результате всё равно вызвал метод и записал в кэш новый результат. Это "user3". Как можно заметить, у него уже новый id. После чего, мы вызываем первый метод, который берёт новый кэш, добавленный "user3".

4. Удаление из кэша. @CacheEvict

Иногда возникает необходимость жёстко обновить какие-то данные в кэше. Например, сущность уже удалена из базы, но она по-прежнему доступна из кэша. Для сохранения консистентности данных, необходимо хотя бы не хранить в кэше удалённые данные.

Добавим в сервис ещё пару методов.

    @Override
    public void delete(Long id) {
        log.info("deleting user by id: {}", id);
        repository.deleteById(id);
    }

    @Override
    @CacheEvict("users")
    public void deleteAndEvict(Long id) {
        log.info("deleting user by id: {}", id);
        repository.deleteById(id);
    }

Первый будет просто удалять пользователя, второй тоже будет его удалять, но мы пометим его аннотацией @CacheEvict. Добавим тест, который будет создавать двух юзеров, после чего, одного будет удалять через простой метод, а второго — через аннотируемый метод. После чего, мы достанем этих юзеров через метод get().

    @Test
    public void delete() {
        User user1 = service.create(new User("Vasya", "[email protected]"));
        log.info("{}", service.get(user1.getId()));

        User user2 = service.create(new User("Vasya", "[email protected]"));
        log.info("{}", service.get(user2.getId()));

        service.delete(user1.getId());
        service.deleteAndEvict(user2.getId());

        log.info("{}", service.get(user1.getId()));
        log.info("{}", service.get(user2.getId()));
    }

Логично, что раз наш юзер уже закэширован, удаление не помешает нам его как бы получить — ведь он закэширован. Посмотрим логи.

getting user by id: 1
User(id=1, name=Vasya, email=vasya@mail.ru)
getting user by id: 2
User(id=2, name=Vasya, email=vasya@mail.ru)
deleting user by id: 1
deleting user by id: 2
User(id=1, name=Vasya, email=vasya@mail.ru)
getting user by id: 2

javax.persistence.EntityNotFoundException: User not found by id 2

Видим, что приложение благополучно сходило оба раза в метод get() и Spring закэшировал эти сущности. Далее, удалили их через разные методы. Первый удалили обычным путём, и закэшированное значение осталось, поэтому когда попытались получить юзера под id 1, это удалось. Когда же попытались получить юзера 2, метод вернул нам EntityNotFoundException — такого юзера в кэше не оказалось.

5. Группировка настроек. @Caching

Иногда один метод требует нескольких настроек кэширования. Для этих целей используется аннотация @Caching. Выглядеть это может приблизительно так:

    @Caching(
            cacheable = {
                    @Cacheable("users"),
                    @Cacheable("contacts")
            },
            put = {
                    @CachePut("tables"),
                    @CachePut("chairs"),
                    @CachePut(value = "meals", key = "#user.email")
            },
            evict = {
                    @CacheEvict(value = "services", key = "#user.name")
            }
    )
    void cacheExample(User user) {
    }

Это единственный способ группировать аннотации. Если попытаемся нагородить что-то вроде

    @CacheEvict("users")
    @CacheEvict("meals")
    @CacheEvict("contacts")
    @CacheEvict("tables")
    void cacheExample(User user) {
    }

то IDEA сообщит, что так нельзя.

6. Гибкая настройка. CacheManager

Рассмотрим как возможно настроить кэширование в целом.

Для таких задач существует CacheManager. Он существует везде, где есть Spring Cache. Когда мы добавили аннотацию @EnableCache, такой кэш менеджер автоматически будет создан Spring. Возможно убедиться в этом, если заавтовайрим ApplicationContext и вскроем его на брейкпоинте. Среди прочих бинов, будет и бин cacheManager.

Остановим приложение на этапе, когда уже два юзера были созданы и помещены в кэш. Если вызовем нужный нам бин через Evaluate Expression, то увидим, что такой бин действительно есть, в нём есть ConcurentMapCache с ключом "users" и значением ConcurrentHashMap, в которой уже лежат закэшированные юзеры.

В свою очередь, можем создать свой кэш-менеджер, с хабром и программистами, после чего, тонко настроить его на наш вкус.

    @Bean("habrCacheManager")
    public CacheManager cacheManager() {
        return null;
    }

Самые популярные кэш-менеджеры:

SimpleCacheManager — самый простой кэш-менеджер, удобный для изучения и тестирования. ConcurrentMapCacheManager — лениво инициализирует возвращаемые экземпляры для каждого запроса. Также рекомендуется для тестирования и изучения работы с кэшем, а также, для каких-то простых действий. Для серьёзной работы с кэшем рекомендуются имплементации ниже. JCacheCacheManager, EhCacheCacheManager, CaffeineCacheManager — серьёзные кэш-менеджеры «от партнёров», гибко настраиваемые и выполняющие задачи очень широкого спектра действия.

Разберём несколько аспектов настройки кэш-менеджера на примере ConcurrentMapCacheManager:

    @Bean("habrCacheManager")
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager();
    }

Таким образом кэш-менеджер готов.

7. Настройка кэша. Время жизни, максимальный размер

Для этого потребуется довольно популярная библиотека Google Guava.

compile group: 'com.google.guava', name: 'guava', version: '28.1-jre'

При создании кэш-менеджера переопределим метод createConcurrentMapCache, в котором вызовем CacheBuilder от Guava.

В процессе нам будет предложено настроить кэш-менеджер при помощи инициализации следующих методов:

  • maximumSize — максимальный размер значений, которые может содержать кэш. При помощи этого параметра можно найти попытаться найти компромисс между нагрузкой на базу данных и на оперативную память JVM.
  • refreshAfterWrite — время после записи значения в кэш, после которого оно автоматически обновится.
  • expireAfterAccess — время жизни значения после последнего обращения к нему.
  • expireAfterWrite — время жизни значения после записи в кэш. Именно этот параметр мы определим.

Определим в менеджере время жизни записи. Например 1 секунду.

    @Bean("habrCacheManager")
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager() {
            @Override
            protected Cache createConcurrentMapCache(String name) {
                return new ConcurrentMapCache(
                        name,
                        CacheBuilder.newBuilder()
                                .expireAfterWrite(1, TimeUnit.SECONDS)
                                .build().asMap(),
                        false);
            }
        };
    }

Напишем соответствующий такому случаю тест.

    @Test
    public void checkSettings() throws InterruptedException {
        User user1 = service.createOrReturnCached(new User("Vasya", "[email protected]"));
        log.info("{}", service.get(user1.getId()));

        User user2 = service.createOrReturnCached(new User("Vasya", "[email protected]"));
        log.info("{}", service.get(user2.getId()));

        Thread.sleep(1000L);
        User user3 = service.createOrReturnCached(new User("Vasya", "[email protected]"));
        log.info("{}", service.get(user3.getId()));
    }

Сохраняем несколько значений в базу данных, причём, если данные закэшированы, ничего не сохраняем. Сначала сохраняем два значения, потом ожидаем 1 секунду, пока кэш не протухнет, после чего, сохраняем ещё одно значение.

creating user: User(id=null, name=Vasya, email=vasya@mail.ru)
getting user by id: 1
User(id=1, name=Vasya, email=vasya@mail.ru)
User(id=1, name=Vasya, email=vasya@mail.ru)
creating user: User(id=null, name=Vasya, email=vasya@mail.ru)
getting user by id: 2
User(id=2, name=Vasya, email=vasya@mail.ru)

Логи показывают, что сначала мы создали юзера, потом попытались ещё одного, но поскольку данные были закэшированы, получили их из кэша (в обоих случаях — при сохранении и при получении из базы). Потом протух кэш, о чём сообщает нам запись о фактическом сохранении и фактическом получении юзера.

Список литературы/курсов

  1. https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#jdbc.why
  2. https://spring-projects.ru/guides/caching/
  3. https://itnan.ru/post.php?c=1&p=427187

Тема 38. Кеширование | Оглавление

⚠️ **GitHub.com Fallback** ⚠️