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

  1. Data Access Layer Design

    • Use repository pattern
    • Implement proper transaction management
    • Handle database exceptions appropriately
    • Use connection pooling
  2. Performance Optimization

    • Use appropriate fetch types
    • Implement caching strategies
    • Optimize queries
    • Use pagination
  3. Data Consistency

    • Implement proper validation
    • Use optimistic locking
    • Handle concurrent modifications
    • Implement audit trails
  4. Security

    • Use prepared statements
    • Implement proper authentication
    • Use encryption when necessary
    • Follow least privilege principle

Common Patterns

  1. 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();
    }
}
  1. 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: