← Назад к вопросам
Почему используешь Spring JDBC в проекте?
2.3 Middle🔥 201 комментариев
#Spring Boot и Spring Data#Spring Framework
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему используешь Spring JDBC в проекте?
Краткий контекст
Предполагаю, что вопрос звучит как: "Почему ты выбрал Spring JDBC вместо других подходов (Hibernate, MyBatis, raw JDBC)?" Это поведенческий и технический вопрос одновременно.
Мой подход к выбору Spring JDBC
Причина 1: Баланс между контролем и удобством
// Raw JDBC - слишком много boilerplate кода
Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE id = ?"
);
stmt.setString(1, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
User user = new User(
rs.getInt("id"),
rs.getString("name"),
rs.getString("email")
);
}
rs.close();
stmt.close();
conn.close(); // Легко забыть
// Spring JDBC - чистый и безопасный код
public User findById(String userId) {
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
new UserRowMapper(),
userId
);
}
// Ресурсы закрываются автоматически
Причина 2: Отсутствие ORM overhead
// Hibernate - мощный но тяжелый
// Проблемы:
// - N+1 query problem
// - Lazy loading surprises
// - Proxy objects complexity
// - Cache invalidation issues
@Entity
@Table(name = "users")
public class User {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // Опасно!
private Department department;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Order> orders; // Потенциально большой список
}
// Spring JDBC - простой и прозрачный
public class UserRowMapper implements RowMapper<User> {
@Override
public User mapRow(ResultSet rs, int rowNum) {
return new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")
); // Явно видно, что загружается
}
}
Причина 3: Производительность
// Hibernate может генерировать неоптимальные запросы
@Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id")
User findWithOrders(Long id);
// Может загрузить 1000 пользователей если есть объединение
// Spring JDBC - только то, что нам нужно
public List<User> findActiveUsers() {
return jdbcTemplate.query(
"SELECT id, name FROM users WHERE active = true LIMIT ?",
new UserRowMapper(),
100
);
}
// Контролируем каждый SELECT
Причина 4: Сложные запросы
// HQL/JPQL для сложных запросов - нечитаемо
@Query(
"SELECT DISTINCT u FROM User u " +
"LEFT JOIN FETCH u.orders o " +
"WHERE u.createdDate > :date " +
"AND u.status IN :statuses " +
"GROUP BY u.id " +
"HAVING COUNT(o) > :minOrders"
)
List<User> findComplexQuery(
LocalDate date,
List<String> statuses,
int minOrders
);
// Spring JDBC - SQL явно видимо
public List<User> findComplexQuery(LocalDate date, List<String> statuses, int minOrders) {
String sql =
"SELECT u.id, u.name, COUNT(o.id) as order_count " +
"FROM users u " +
"LEFT JOIN orders o ON u.id = o.user_id " +
"WHERE u.created_date > ? " +
"AND u.status IN (?, ?, ?) " +
"GROUP BY u.id " +
"HAVING COUNT(o.id) > ? " +
"ORDER BY order_count DESC";
return jdbcTemplate.query(
sql,
new UserRowMapper(),
date,
statuses.get(0),
statuses.get(1),
statuses.get(2),
minOrders
);
}
// Все ясно и видимо
Проблемы Hibernate, которые избегу с Spring JDBC
Проблема 1: N+1 Query Problem
// Hibernate - неоптимальный код
@Entity
public class User {
@OneToMany(cascade = CascadeType.ALL)
private List<Order> orders;
}
// Запрос:
List<User> users = userRepository.findAll(); // SELECT * FROM users (1 запрос)
for (User user : users) {
System.out.println(user.getOrders()); // SELECT * FROM orders WHERE user_id = ?
// Если пользователей 1000, то 1001 запрос!
}
// Spring JDBC - контролируем явно
public List<User> findAllWithOrders() {
String sql =
"SELECT u.id, u.name, o.id as order_id, o.amount " +
"FROM users u " +
"LEFT JOIN orders o ON u.id = o.user_id " +
"ORDER BY u.id";
return jdbcTemplate.query(sql, rs -> {
Map<Long, User> users = new LinkedHashMap<>();
while (rs.next()) {
Long userId = rs.getLong("id");
User user = users.computeIfAbsent(
userId,
k -> new User(userId, rs.getString("name"))
);
Long orderId = rs.getLong("order_id");
if (orderId > 0) {
user.addOrder(new Order(orderId, rs.getBigDecimal("amount")));
}
}
return new ArrayList<>(users.values());
});
// Один запрос, полный контроль!
}
Проблема 2: Lazy Loading
// Hibernate - неожиданные запросы
@Entity
public class User {
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
}
User user = userRepository.findById(1).get();
System.out.println(user.getId()); // OK
System.out.println(user.getDepartment().getName()); // ДОПОЛНИТЕЛЬНЫЙ ЗАПРОС!
// LazyInitializationException если сессия закрыта
// Spring JDBC - загружаешь то, что нужно
public class UserRow {
private Long id;
private String name;
private Long departmentId; // ID, не объект
private String departmentName; // Если нужно
}
public UserRow findUserById(Long userId) {
return jdbcTemplate.queryForObject(
"SELECT u.id, u.name, d.id as department_id, d.name as department_name " +
"FROM users u " +
"LEFT JOIN departments d ON u.department_id = d.id " +
"WHERE u.id = ?",
new UserRowMapper(),
userId
);
}
Когда использую Spring JDBC
Сценарий 1: OLTP приложение с простыми операциями
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void save(User user) {
jdbcTemplate.update(
"INSERT INTO users (name, email) VALUES (?, ?)",
user.getName(),
user.getEmail()
);
}
public Optional<User> findById(Long id) {
try {
User user = jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
new UserRowMapper(),
id
);
return Optional.of(user);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
}
Сценарий 2: Batch операции
public void batchInsertUsers(List<User> users) {
jdbcTemplate.batchUpdate(
"INSERT INTO users (name, email) VALUES (?, ?)",
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) {
ps.setString(1, users.get(i).getName());
ps.setString(2, users.get(i).getEmail());
}
@Override
public int getBatchSize() {
return users.size();
}
}
);
}
Сценарий 3: Аналитические запросы
public List<UserStatistic> getUserStatistics(LocalDate from, LocalDate to) {
String sql =
"SELECT " +
" DATE(created_date) as date, " +
" COUNT(DISTINCT id) as total_users, " +
" SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_users, " +
" AVG(order_count) as avg_orders " +
"FROM users " +
"WHERE created_date BETWEEN ? AND ? " +
"GROUP BY DATE(created_date) " +
"ORDER BY date DESC";
return jdbcTemplate.query(
sql,
new StatisticRowMapper(),
from,
to
);
}
Сравнение подходов
// ❌ Raw JDBC
// Плюсы: Полный контроль
// Минусы: Много boilerplate, легко ошибиться
// ✅ Spring JDBC
// Плюсы: Простой, безопасный, контролируемый
// Минусы: Нет автоматического маппинга
// ⚠️ Hibernate
// Плюсы: Мощный, автоматический маппинг
// Минусы: Heavy, сложно отловить проблемы, N+1 problem
// ℹ️ MyBatis
// Плюсы: Контроль как в Spring JDBC но с кешированием
// Минусы: Больше боilerplate чем Spring JDBC
Практические примеры из опыта
Пример 1: RowMapper для переиспользования
@Component
public class UserRowMapper implements RowMapper<User> {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
return User.builder()
.id(rs.getLong("id"))
.name(rs.getString("name"))
.email(rs.getString("email"))
.createdAt(rs.getTimestamp("created_at").toLocalDateTime())
.build();
}
}
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
private final UserRowMapper rowMapper;
public List<User> findAll() {
return jdbcTemplate.query(
"SELECT * FROM users",
rowMapper // Переиспользуем RowMapper
);
}
}
Пример 2: Named parameters для читаемости
@Repository
public class UserRepository {
private final NamedParameterJdbcTemplate namedJdbcTemplate;
public List<User> findByFilters(String status, LocalDate from, LocalDate to) {
String sql =
"SELECT * FROM users " +
"WHERE status = :status " +
"AND created_date BETWEEN :from AND :to";
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("status", status)
.addValue("from", from)
.addValue("to", to);
return namedJdbcTemplate.query(sql, params, new UserRowMapper());
}
}
Почему я предпочитаю Spring JDBC в своих проектах
- Явность — видно что загружается, какие query выполняются
- Производительность — нет неожиданных запросов, полный контроль
- Простота — меньше магии, меньше surprises
- Тестируемость — легко мокировать JdbcTemplate
- Отсутствие боли — нет N+1 problem, lazy loading issues, cache invalidation
- Масштабируемость — когда много пользователей важна каждая миллисекунда
- Командный опыт — все понимают обычный SQL
- Отладка — SQL явно видим, легко профилировать
Когда я могу использовать Hibernate
- Простой CRUD проект
- Нет сложных запросов
- Производительность не критична
- Быстрое прототипирование
Заключение
Spring JDBC — это не устарелый инструмент. Это правильный выбор для:
- Производственных приложений, где производительность важна
- Проектов со сложными аналитическими запросами
- Когда нужна полная контрольность над запросами
- Когда team ценит явность и простоту
Использование Spring JDBC показывает, что ты понимаешь trade-offs между удобством и контролем, и выбираешь правильный инструмент для задачи, а не просто берешь популярный ORM.