Skip to content

Commit

Permalink
feat(cache): implement caching system using Redis for company and emp…
Browse files Browse the repository at this point in the history
…loyee services

- added Redis cache configuration in `application-dev.yml`, `application-prod.yml`, and `application-test.yml`
- enabled caching in the Spring Boot application with `@EnableCaching`
- introduced `spring-boot-starter-data-redis` dependency in `pom.xml` for Redis integration
- implemented caching for `CompanyService` and `EmployeeService` using `@Cacheable`, `@CacheEvict`
- cache added for fetching companies and employees, with methods caching paginated results and searching by name
- adjusted Docker Compose files to include Redis service for local and production environments

Closes #10
  • Loading branch information
gabrizord authored Sep 14, 2024
1 parent 890604a commit b618d1f
Show file tree
Hide file tree
Showing 13 changed files with 280 additions and 61 deletions.
25 changes: 11 additions & 14 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,37 +1,34 @@
# Multi-stage Dockerfile: Build and run a Java application
# Docker image build for GZGest o
#
# This Dockerfile creates the GZGest o image using the following steps:
#
# 1. Maven 3.9.8-eclipse-temurin-21-jammy base image
# 2. Copy pom.xml and install dependencies using offline mode
# 3. Copy source code and build the application
# 4. Copy the JAR file to the /app directory
# 5. Create a user and change to that user
# 6. Start the application with the JAR file

# Stage 1: Use Maven to build the application, skipping tests.
FROM maven:3.9.8-eclipse-temurin-21-jammy AS build

# Set the working directory
WORKDIR /app

# Copy the POM file first
COPY pom.xml ./

# Cache Maven dependencies
RUN mvn dependency:go-offline -B

# Copy the source code only if dependencies are resolved
COPY src ./src

# Build the application, skipping tests
RUN mvn package -DskipTests -B

# Stage 2: Run the application using a non-root user
FROM eclipse-temurin:21-jre-jammy

# Set the working directory
WORKDIR /app

# Copy the JAR file from the build stage
COPY --from=build /app/target/*.jar app.jar

# Create a non-root user
RUN useradd -m myuser

# Set the user
USER myuser

# Set the entrypoint
ENTRYPOINT ["java", "-jar", "app.jar"]
ENTRYPOINT ["java", "-jar", "app.jar"]
29 changes: 26 additions & 3 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ services:
deploy:
resources:
limits:
cpus: '1'
memory: '8g'
cpus: '1.5'
memory: '6g'
reservations:
cpus: '1'
memory: '6g'
memory: '4g'
depends_on:
- db
- cache-redis

db:
image: postgres:latest
Expand All @@ -30,6 +31,28 @@ services:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
deploy:
resources:
limits:
cpus: '0.3'
memory: '1g'
reservations:
cpus: '0.2'
memory: '512m'

cache-redis:
image: redis:7.4
container_name: redis-cache
ports:
- "6379:6379"
deploy:
resources:
limits:
cpus: '0.2'
memory: '512m'
reservations:
cpus: '0.1'
memory: '256m'

volumes:
postgres-data:
18 changes: 17 additions & 1 deletion docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ services:
app:
image: ${DOCKER_IMAGE}
container_name: springboot-app
restart: always
ports:
- "8080:8080"
environment:
Expand All @@ -16,8 +17,23 @@ services:
deploy:
resources:
limits:
cpus: '1'
cpus: '1.5'
memory: '8g'
reservations:
cpus: '1'
memory: '6g'

cache-redis:
image: redis:7.4
restart: always
container_name: cache-redis
ports:
- "6379:6379"
deploy:
resources:
limits:
cpus: '0.5'
memory: '2g'
reservations:
cpus: '0.3'
memory: '1g'
14 changes: 4 additions & 10 deletions docker-dev.env
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,17 @@
# and Spring Boot when the application is running in a development environment.
# Remember not to version this file, as it may contain sensitive information.
#
# List of keys:
# - SPRING_DATASOURCE_URL: Database connection URL for local development.
# - SPRING_DATASOURCE_USERNAME: Username for the local database.
# - SPRING_DATASOURCE_PASSWORD: Password for the local database.
# - POSTGRES_DB: Name of the PostgreSQL database for local development.
# - POSTGRES_USER: Username for the PostgreSQL database.
# - POSTGRES_PASSWORD: Password for the PostgreSQL database.
# - JWT_PUBLIC_KEY: Public key for JWT validation.
# - JWT_PRIVATE_KEY: Private key for JWT signing.
# - OTHER_KEYS: Include any other keys necessary for local development.

SPRING_PROFILES_ACTIVE: dev
SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/database
SPRING_DATASOURCE_USERNAME=user
SPRING_DATASOURCE_PASSWORD=password
SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.postgresql.Driver
SPRING_JPA_HIBERNATE_DDL_AUTO=update
SPRING_CACHE_TYPE=redis
SPRING_DATA_REDIS_HOST=cache-redis
SPRING_DATA_REDIS_PORT=6379


POSTGRES_DB:database
POSTGRES_USER:user
Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@
<version>3.1.0</version>
</dependency>

<!-- Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Testing -->
<dependency>
<groupId>org.springframework.security</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class GzGestaoApplication {

public static void main(String[] args) {
Expand Down
39 changes: 24 additions & 15 deletions src/main/java/br/com/gabrizord/gzgestao/service/CompanyService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import br.com.gabrizord.gzgestao.model.Company;
import br.com.gabrizord.gzgestao.repository.CompanyRepository;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
Expand All @@ -18,12 +20,14 @@
@Service
public class CompanyService {

private static final String COMPANY_NOT_FOUND_MESSAGE = "Company not found.";
CompanyRepository companyRepository;

public CompanyService(CompanyRepository companyRepository) {
this.companyRepository = companyRepository;
}

@CacheEvict(value = {"companies", "companyById", "companiesByName"}, allEntries = true)
public Company saveCompany(CompanyDTO companyDTO) {
companyRepository.findByCnpj(companyDTO.getCnpj())
.ifPresent(company -> {
Expand All @@ -32,22 +36,25 @@ public Company saveCompany(CompanyDTO companyDTO) {
return companyRepository.save(companyDTO.convertToEntity());
}

@Cacheable(value = "companies", key = "'all'")
public List<Company> getAllCompanies() {
return companyRepository.findAll();
}

@Cacheable(value = "companyById", key = "#id")
public Company getCompanyById(Long id) {
Optional<Company> company = companyRepository.findById(id);
return company.orElseThrow(() -> new EntityNotFoundException("Empresa não encontrada."));
return company.orElseThrow(() -> new EntityNotFoundException(COMPANY_NOT_FOUND_MESSAGE));
}

@CacheEvict(value = {"companies", "companyById", "companiesByName"}, key = "#id", allEntries = true)
public void deleteCompany(Long id) {
Company company = getCompanyById(id);
companyRepository.delete(company);
companyRepository.delete(companyRepository.findById(id).orElseThrow(() -> new EntityNotFoundException(COMPANY_NOT_FOUND_MESSAGE)));
}

@CacheEvict(value = {"companies", "companyById", "companiesByName"}, key = "#id", allEntries = true)
public Company updateCompany(Long id, CompanyUpdateDTO companyDTO) {
Company existingCompany = getCompanyById(id);
Company existingCompany = companyRepository.findById(id).orElseThrow(() -> new EntityNotFoundException(COMPANY_NOT_FOUND_MESSAGE));

if (shouldUpdateCnpj(companyDTO, existingCompany)) {
validateCnpjChange(existingCompany, companyDTO.getCnpj());
Expand All @@ -58,6 +65,18 @@ public Company updateCompany(Long id, CompanyUpdateDTO companyDTO) {
return companyRepository.save(existingCompany);
}

@Cacheable(value = "companies", key = "#page + '-' + #size + '-' + #sortField + '-' + #sortDirection")
public Page<Company> getPaginatedCompanies(int page, int size, String sortField, String sortDirection) {
Sort sort = sortDirection.equalsIgnoreCase(Sort.Direction.ASC.name()) ? Sort.by(sortField).ascending() : Sort.by(sortField).descending();
Pageable pageable = PageRequest.of(page, size, sort);
return companyRepository.findAll(pageable);
}

@Cacheable(value = "companiesByName", key = "#name")
public List<Company> findByNameContainingIgnoreCase(String name) {
return companyRepository.findByRazaoSocialContainingIgnoreCase(name);
}

private boolean shouldUpdateCnpj(CompanyUpdateDTO companyDTO, Company existingCompany) {
return companyDTO.getCnpj() != null && !companyDTO.getCnpj().equals(existingCompany.getCnpj());
}
Expand All @@ -84,7 +103,7 @@ private <T> void updateFieldIfNecessary(T newValue, T currentValue, Consumer<T>
}
}

private void validateCnpjChange(Company existingCompany, String cnpj) {
private void validateCnpjChange(Company existingCompany, String cnpj) {
if (!existingCompany.getCnpj().equals(cnpj)) {
companyRepository.findByCnpj(cnpj).ifPresent(company -> {
if (!company.getId().equals(existingCompany.getId())) {
Expand All @@ -93,14 +112,4 @@ private void validateCnpjChange(Company existingCompany, String cnpj) {
});
}
}

public Page<Company> getPaginatedCompanies(int page, int size, String sortField, String sortDirection) {
Sort sort = sortDirection.equalsIgnoreCase(Sort.Direction.ASC.name()) ? Sort.by(sortField).ascending() : Sort.by(sortField).descending();
Pageable pageable = PageRequest.of(page, size, sort);
return companyRepository.findAll(pageable);
}

public List<Company> findByNameContainingIgnoreCase(String name) {
return companyRepository.findByRazaoSocialContainingIgnoreCase(name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import br.com.gabrizord.gzgestao.model.Employee;
import br.com.gabrizord.gzgestao.repository.EmployeeRepository;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
Expand All @@ -23,25 +25,30 @@ public EmployeeService(EmployeeRepository employeeRepository) {
this.employeeRepository = employeeRepository;
}

@CacheEvict(value = {"employees", "employeesByName"}, allEntries = true)
public Employee createEmployee(EmployeeDTO employeeDTO) {
employeeRepository.findByEmail(employeeDTO.getEmail()).ifPresent(employee -> {
throw new IllegalArgumentException("Já existe um funcionário com este email.");
});
return employeeRepository.save(employeeDTO.convertToEntity());
}

@Cacheable(value = "employees", key = "'all'")
public List<Employee> getAllEmployees() {
return employeeRepository.findAll();
}

@Cacheable(value = "employee", key = "#id")
public Optional<Employee> getEmployeeById(Long id) {
return employeeRepository.findById(id);
}

@CacheEvict(value = {"employee", "employees", "employeesByName"}, key = "#id", allEntries = true)
public void deleteEmployee(Long id) {
employeeRepository.deleteById(id);
}

@CacheEvict(value = {"employee", "employees", "employeesByName"}, key = "#id", allEntries = true)
public Employee updateEmployee(Long id, EmployeeUpdateDTO employeeDTO) {
Employee existingEmployee = employeeRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Funcionário não encontrado"));
Expand All @@ -56,6 +63,7 @@ public Employee updateEmployee(Long id, EmployeeUpdateDTO employeeDTO) {
return employeeRepository.save(existingEmployee);
}

@Cacheable(value = "employees", key = "#page + '-' + #size + '-' + #sortField + '-' + #sortDirection")
public Page<Employee> getPaginatedEmployees(int page, int size, String sortField, String sortDirection) {
Sort sort = sortDirection.equalsIgnoreCase(Sort.Direction.ASC.name()) ? Sort.by(sortField).ascending() : Sort.by(sortField).descending();
Pageable pageable = PageRequest.of(page, size, sort);
Expand Down Expand Up @@ -84,6 +92,7 @@ private void updateEmployeeFields(Employee existingEmployee, EmployeeUpdateDTO e
}
}

@Cacheable(value = "employeesByName", key = "#name")
public List<Employee> findByNameContainingIgnoreCase(String name) {
return employeeRepository.findByNameContainingIgnoreCase(name);
}
Expand Down
28 changes: 25 additions & 3 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,32 @@ spring:
password: password
driver-class-name: org.postgresql.Driver

cache:
type: redis
redis:
time-to-live: 2m
data:
redis:
host: localhost
port: 6379
password: ""

sql:
init:
mode: always
data-locations: classpath:data-dev.sql

jpa:
hibernate:
ddl-auto: update
show-sql: true
open-in-view: false
defer-datasource-initialization: true

h2:
console:
enabled: false

thymeleaf:
cache: false

Expand All @@ -34,6 +53,9 @@ spring:
additional-exclude:
- custom-dir/**,another-dir/**

h2:
console:
enabled: false
logging:
level:
root: INFO
org.springframework.security: WARN
org.hibernate: WARN
com.zaxxer.hikari: INFO
Loading

0 comments on commit b618d1f

Please sign in to comment.