Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions data-redis-cache/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary
37 changes: 37 additions & 0 deletions data-redis-cache/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/
34 changes: 34 additions & 0 deletions data-redis-cache/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.3'
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'zin.rashidi'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:postgresql'
testImplementation 'com.redis:testcontainers-redis'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
useJUnitPlatform()
}
1 change: 1 addition & 0 deletions data-redis-cache/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'data-redis-cache'
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package zin.rashidi.dataredis.cache;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DataRedisCacheApplication {

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package zin.rashidi.dataredis.cache.cache;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;

/**
* @author Rashidi Zin
*/
@Configuration
@EnableCaching
class CacheConfiguration {

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager.create(connectionFactory);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The RedisCacheManager.create() method has been deprecated since Spring Framework 6.1. It's recommended to use the builder pattern, which provides more flexibility for configuration.

Suggested change
return RedisCacheManager.create(connectionFactory);
return RedisCacheManager.builder(connectionFactory).build();

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package zin.rashidi.dataredis.cache.customer;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

import java.io.Serializable;

/**
* @author Rashidi Zin
*/
@Entity
class Customer implements Serializable {

@Id
@GeneratedValue
private Long id;

private String name;

@Override
public final boolean equals(Object o) {
return o instanceof Customer another && this.id.equals(another.id);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This implementation will throw a NullPointerException if this.id is null, which is the case for a new entity before it's persisted. You should add a null-check for the id before calling .equals() on it to prevent this.

Suggested change
return o instanceof Customer another && this.id.equals(another.id);
return o instanceof Customer another && id != null && id.equals(another.id);

}

@Override
public int hashCode() {
return id.hashCode();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This hashCode implementation will throw a NullPointerException if id is null. Furthermore, for JPA entities with generated IDs, the hash code will change after the entity is persisted. This can cause issues if the entity is stored in a HashSet or as a key in a HashMap.

A common and safer practice is to use a constant hash code that doesn't depend on the generated ID, ensuring the hash code is stable throughout the entity's lifecycle.

Suggested change
return id.hashCode();
return getClass().hashCode();

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package zin.rashidi.dataredis.cache.customer;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

/**
* @author Rashidi Zin
*/
interface CustomerRepository extends JpaRepository<Customer, Long> {

@Override
@Cacheable(cacheNames = "customers", key = "#root.methodName")
List<Customer> findAll();

@Override
@Cacheable(cacheNames = "customerById")
Optional<Customer> findById(Long id);

}
1 change: 1 addition & 0 deletions data-redis-cache/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
spring.application.name=data-redis-cache
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package zin.rashidi.dataredis.cache;

import org.springframework.boot.SpringApplication;

public class TestDataRedisCacheApplication {

public static void main(String[] args) {
SpringApplication.from(DataRedisCacheApplication::main).with(TestcontainersConfiguration.class).run(args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package zin.rashidi.dataredis.cache;

import com.redis.testcontainers.RedisContainer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;

@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfiguration {

@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));
}

@Bean
@ServiceConnection(name = "redis")
RedisContainer redisContainer() {
return new RedisContainer(DockerImageName.parse("redis:latest"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package zin.rashidi.dataredis.cache.customer;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.transaction.annotation.Transactional;
import zin.rashidi.dataredis.cache.TestcontainersConfiguration;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS;

/**
* @author Rashidi Zin
*/
@Import(TestcontainersConfiguration.class)
@ImportAutoConfiguration({ RedisAutoConfiguration.class, CacheAutoConfiguration.class })
@Sql(executionPhase = BEFORE_TEST_CLASS, statements = "INSERT INTO customer (id, name) VALUES (1, 'Rashidi Zin')")
@DataJpaTest(properties = "spring.jpa.hibernate.ddl-auto=create-drop", includeFilters = @Filter(EnableCaching.class))
class CustomerRepositoryTests {

@Autowired
private CustomerRepository customers;

@Autowired
private CacheManager caches;

@Test
@Transactional(readOnly = true)
@DisplayName("Given the method name is configured as the cache's key Then subsequent retrieval should return the same value as initial retrieval")
void findAll() {
var persisted = customers.findAll();
var cached = caches.getCache("customers").get("findAll").get();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Directly chaining .get() can lead to a NullPointerException if the cache entry for the key "findAll" does not exist, as caches.getCache("customers").get("findAll") would return null. This can make test failures harder to diagnose.

A more robust approach would be to verify the ValueWrapper is not null before getting its value.


assertThat(cached).isEqualTo(persisted);
}

@Test
@Transactional(readOnly = true)
@DisplayName("Given the cache is configured Then subsequent retrieval with the same key should return the same value as initial retrieval")
void findById() {
var persisted = customers.findById(1L).get();
var cached = caches.getCache("customerById").get(1L).get();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Directly chaining .get() can lead to a NullPointerException if the cache entry for the key 1L does not exist, as caches.getCache("customerById").get(1L) would return null. This can make test failures harder to diagnose.

A more robust approach would be to verify the ValueWrapper is not null before getting its value.


assertThat(cached).isEqualTo(persisted);
}

}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ include("data-mongodb-audit")
include("data-mongodb-full-text-search")
include("data-mongodb-tc-data-load")
include("data-mongodb-transactional")
include("data-redis-cache")
include("data-repository-definition")
include("data-rest-composite-id")
include("data-rest-validation")
Expand Down
Loading