Introduction
Hello Java developers, accessing the Database is crucial but what are the best practices? In this article, we will dive into the Java database best practices that are essential for every developer aiming to master database interactions and ORM (Object-Relational Mapping) in Java. By adopting these best practices, you’ll enhance not only the performance of your applications but also their maintainability. We’ll cover the key techniques and practices such as abstraction, transaction management, lazy loading, and null handling, which will help you effectively manage common challenges in database access and ORM (Object-Relational Mapping).
1. Using Repository Abstraction
When using frameworks such as Spring, Micronaut, or Quarkus it is recommended to use the Data repository interfaces, such as JpaRepository, which allows for a more abstract approach to database access, leading to cleaner and more maintainable application code.
✅ Good: Interface-based repositories enable a cleaner separation of concerns
public interface UserRepository extends JpaRepository<User, Long> { Optional <User> findByEmail(String email); }
@Service public class UserService { private final UserRepository userRepository; @Autowired public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User getUserByEmail(String email) { return userRepository.findByEmail(email) .orElseThrow(() -> new UserNotFoundException("User not found")); } }
⛔ Avoid: Direct use of EntityManager results in error-prone and less manageable code
@Service public class UserService { @Autowired private EntityManager entityManager; public User getUserByEmail(String email) { // Bad: Using EntityManager directly for something repositories could do Query query = entityManager.createQuery("SELECT u FROM User u WHERE u.email = :email"); query.setParameter("email", email); return (User) query.getSingleResult(); } }
2. Transaction Management
✅ Good: Proper use of @Transactional at the service layer
@Service @Transactional public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User createUser(User user) { // The transaction is automatically managed by Spring return userRepository.save(user); } }
⛔ Avoid: Annotating individual repository methods with @Transactional is unnecessary
public interface UserRepository extends JpaRepository<User, Long> { @Transactional <extends User S> save(S entity); }
3. Handling Lazy Initialization
✅ Good: Load lazy associations within a transaction using Hibernate.initialize()
@Service public class UserService { private final UserRepository userRepository; @Transactional(readOnly = true) public User getUserWithOrders(Long userId) { User user = userRepository.findById(userId) .orElseThrow(() - new UserNotFoundException("User not found")); // Initialize lazy collection Hibernate.initialize(user.getOrders()); return user; } }
Explanation: This is good because it handles the lazy loading within a transactional context, ensuring that the Hibernate.initialize() method can load the lazy collection before the session is closed. The use of readOnly = true is also good practice for read operations as it can optimize performance.
⛔ Avoid: Accessing a lazily loaded collection outside a transaction can lead to exceptions
@Service public class UserService { @Autowired private UserRepository userRepository; public User getUserWithOrders(Long userId) { User user = userRepository.findById(userId).get(); // May throw LazyInitializationException int orderCount = user.getOrders().size(); return user; } }
4. Using Pagination
✅ Good: Implementing pagination with Spring Data’s Pageable
Pagination helps in fetching data in manageable chunks, thus saving resources.
public interface UserRepository extends JpaRepository<User, Long> { PageUser findAll(Pageable pageable); } @Service public class UserService { private final UserRepository userRepository; public Page<User> getUsersWithPagination(int page, int size) { Pageable pageable = PageRequest.of(page, size, Sort.by("lastName").ascending()); return userRepository.findAll(pageable); } }
⛔ Avoid: Retrieving all entries can lead to memory and performance issues
@RestController public class UserController { private final UserRepository userRepository; @GetMapping("/users") public <List> User getAllUsers() { } }
5. Handling Null Values with Optional
Check out here for more details dealing with null values.
✅ Good: Using orElseThrow() to handle absent values elegantly
@Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User getUserById(Long id) { return userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException("User with id " + id + " not found")); } }
⛔ Avoid: Using get() without checking if the value is present can cause exceptions
@Service public class UserService { private final UserRepository userRepository; public User getUserById(Long id) { return userRepository.findById(id).get(); } }