Spring Boot Data Access Guide
Comprehensive guide to data access in Spring Boot applications using JPA, MongoDB, Redis, and Elasticsearch
Spring Boot Data Access Guide
This guide covers different data access approaches in Spring Boot applications, including JPA/Hibernate, MongoDB, Redis, and Elasticsearch implementations.
Video Tutorial
Learn more about Spring Boot data access in this comprehensive video tutorial:
Prerequisites
<dependencies>
<!-- JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MongoDB -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
JPA/Hibernate Implementation
Entity Definition
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private BigDecimal price;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Review> reviews = new ArrayList<>();
@Version
private Long version;
// Getters, setters, and constructors
}
Repository Layer
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
Optional<Product> findByName(String name);
List<Product> findByCategoryId(Long categoryId);
@Query("SELECT p FROM Product p WHERE p.price BETWEEN :minPrice AND :maxPrice")
List<Product> findByPriceRange(
@Param("minPrice") BigDecimal minPrice,
@Param("maxPrice") BigDecimal maxPrice);
@Modifying
@Query("UPDATE Product p SET p.price = :newPrice WHERE p.id = :id")
int updatePrice(@Param("id") Long id, @Param("newPrice") BigDecimal newPrice);
}
Service Layer with Transaction Management
@Service
@Transactional
public class ProductService {
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
public Product createProduct(ProductDTO dto) {
Category category = categoryRepository.findById(dto.getCategoryId())
.orElseThrow(() -> new CategoryNotFoundException(dto.getCategoryId()));
Product product = new Product();
product.setName(dto.getName());
product.setPrice(dto.getPrice());
product.setCategory(category);
return productRepository.save(product);
}
@Transactional(readOnly = true)
public Page<Product> findProducts(ProductCriteria criteria, Pageable pageable) {
return productRepository.findAll(
createSpecification(criteria),
pageable);
}
private Specification<Product> createSpecification(ProductCriteria criteria) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (criteria.getName() != null) {
predicates.add(cb.like(root.get("name"),
"%" + criteria.getName() + "%"));
}
if (criteria.getMinPrice() != null) {
predicates.add(cb.greaterThanOrEqualTo(
root.get("price"), criteria.getMinPrice()));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
}
MongoDB Implementation
Document Definition
@Document(collection = "products")
public class Product {
@Id
private String id;
@Indexed(unique = true)
private String sku;
private String name;
private BigDecimal price;
@DBRef
private Category category;
private List<Review> reviews = new ArrayList<>();
@Version
private Long version;
// Getters, setters, and constructors
}
MongoDB Repository
@Repository
public interface ProductRepository extends MongoRepository<Product, String> {
Optional<Product> findBySku(String sku);
@Query("{'price': {$gte: ?0, $lte: ?1}}")
List<Product> findByPriceRange(BigDecimal minPrice, BigDecimal maxPrice);
@Query(value = "{'category.$id': ?0}", fields = "{'name': 1, 'price': 1}")
List<Product> findByCategoryId(String categoryId);
}
MongoDB Service Layer
@Service
public class ProductService {
private final ProductRepository productRepository;
private final MongoTemplate mongoTemplate;
public Product createProduct(ProductDTO dto) {
Product product = new Product();
product.setSku(generateSku());
product.setName(dto.getName());
product.setPrice(dto.getPrice());
return productRepository.save(product);
}
public List<Product> findByComplexCriteria(ProductCriteria criteria) {
Query query = new Query();
if (criteria.getName() != null) {
query.addCriteria(Criteria.where("name")
.regex(criteria.getName(), "i"));
}
if (criteria.getMinPrice() != null) {
query.addCriteria(Criteria.where("price")
.gte(criteria.getMinPrice()));
}
return mongoTemplate.find(query, Product.class);
}
}
Redis Implementation
Redis Configuration
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Key serializer
template.setKeySerializer(new StringRedisSerializer());
// Value serializer
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
template.setValueSerializer(serializer);
return template;
}
}
Redis Entity
@RedisHash("products")
public class Product implements Serializable {
@Id
private String id;
private String name;
private BigDecimal price;
@TimeToLive
private Long ttl;
// Getters, setters, and constructors
}
Redis Repository
@Repository
public interface ProductRedisRepository extends CrudRepository<Product, String> {
List<Product> findByName(String name);
}
Redis Service Layer
@Service
public class ProductCacheService {
private final RedisTemplate<String, Object> redisTemplate;
private final ProductRedisRepository redisRepository;
public void cacheProduct(Product product) {
String key = "product:" + product.getId();
redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS);
}
public Optional<Product> getFromCache(String id) {
String key = "product:" + id;
Product product = (Product) redisTemplate.opsForValue().get(key);
return Optional.ofNullable(product);
}
public void invalidateCache(String id) {
String key = "product:" + id;
redisTemplate.delete(key);
}
}
Elasticsearch Implementation
Elasticsearch Document
@Document(indexName = "products")
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "standard")
private String name;
@Field(type = FieldType.Keyword)
private String sku;
@Field(type = FieldType.Double)
private BigDecimal price;
@Field(type = FieldType.Nested)
private List<Review> reviews;
// Getters, setters, and constructors
}
Elasticsearch Repository
@Repository
public interface ProductSearchRepository extends ElasticsearchRepository<Product, String> {
List<Product> findByNameContaining(String name);
@Query("{\"bool\": {\"must\": [{\"range\": {\"price\": {\"gte\": \"?0\", \"lte\": \"?1\"}}}]}}")
List<Product> findByPriceRange(BigDecimal minPrice, BigDecimal maxPrice);
}
Elasticsearch Service Layer
@Service
public class ProductSearchService {
private final ProductSearchRepository searchRepository;
private final ElasticsearchRestTemplate elasticsearchTemplate;
public List<Product> searchProducts(String query) {
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.multiMatchQuery(query)
.field("name", 2.0f)
.field("description")
.type(MultiMatchQueryBuilder.Type.BEST_FIELDS))
.withHighlightFields(
new HighlightBuilder.Field("name"),
new HighlightBuilder.Field("description"))
.build();
SearchHits<Product> searchHits = elasticsearchTemplate
.search(searchQuery, Product.class);
return searchHits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
public List<Product> findSimilarProducts(String productId) {
Product product = searchRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.moreLikeThisQuery(
new String[]{"name", "description"},
new String[]{product.getName(), product.getDescription()})
.minTermFreq(1)
.maxQueryTerms(12))
.build();
return elasticsearchTemplate.search(searchQuery, Product.class)
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
}
Multi-Database Configuration
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = "com.example.repository.jpa",
entityManagerFactoryRef = "jpaEntityManagerFactory",
transactionManagerRef = "jpaTransactionManager"
)
@EnableMongoRepositories(
basePackages = "com.example.repository.mongo",
mongoTemplateRef = "mongoTemplate"
)
@EnableElasticsearchRepositories(
basePackages = "com.example.repository.search"
)
public class DatabaseConfig {
@Primary
@Bean
public LocalContainerEntityManagerFactoryBean jpaEntityManagerFactory() {
// JPA configuration
}
@Bean
public MongoTemplate mongoTemplate() {
// MongoDB configuration
}
@Bean
public ElasticsearchOperations elasticsearchTemplate() {
// Elasticsearch configuration
}
}
Best Practices
-
Data Access Layer Design
- Use repository pattern
- Implement proper transaction management
- Handle database exceptions appropriately
- Use connection pooling
-
Performance Optimization
- Use appropriate fetch types
- Implement caching strategies
- Optimize queries
- Use pagination
-
Data Consistency
- Implement proper validation
- Use optimistic locking
- Handle concurrent modifications
- Implement audit trails
-
Security
- Use prepared statements
- Implement proper authentication
- Use encryption when necessary
- Follow least privilege principle
Common Patterns
- Repository Pattern with Specification
public interface CustomProductRepository {
List<Product> findBySpecification(ProductSpecification spec);
}
@Repository
public class CustomProductRepositoryImpl implements CustomProductRepository {
@PersistenceContext
private EntityManager em;
@Override
public List<Product> findBySpecification(ProductSpecification spec) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Product> query = cb.createQuery(Product.class);
Root<Product> root = query.from(Product.class);
query.where(spec.toPredicate(root, query, cb));
return em.createQuery(query).getResultList();
}
}
- Caching Strategy
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
@CacheEvict(value = "products", key = "#id")
public void updateProduct(Long id, ProductDTO dto) {
Product product = getProduct(id);
updateProductFromDTO(product, dto);
productRepository.save(product);
}
@CacheEvict(value = "products", allEntries = true)
public void refreshCache() {
// Refresh cache logic
}
}
Conclusion
Effective data access in Spring Boot requires:
- Understanding different database options
- Proper configuration and setup
- Implementation of best practices
- Performance optimization
- Security considerations
For more Spring Boot topics, check out: