← Назад к вопросам

Почему используешь 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 в своих проектах

  1. Явность — видно что загружается, какие query выполняются
  2. Производительность — нет неожиданных запросов, полный контроль
  3. Простота — меньше магии, меньше surprises
  4. Тестируемость — легко мокировать JdbcTemplate
  5. Отсутствие боли — нет N+1 problem, lazy loading issues, cache invalidation
  6. Масштабируемость — когда много пользователей важна каждая миллисекунда
  7. Командный опыт — все понимают обычный SQL
  8. Отладка — SQL явно видим, легко профилировать

Когда я могу использовать Hibernate

  • Простой CRUD проект
  • Нет сложных запросов
  • Производительность не критична
  • Быстрое прототипирование

Заключение

Spring JDBC — это не устарелый инструмент. Это правильный выбор для:

  • Производственных приложений, где производительность важна
  • Проектов со сложными аналитическими запросами
  • Когда нужна полная контрольность над запросами
  • Когда team ценит явность и простоту

Использование Spring JDBC показывает, что ты понимаешь trade-offs между удобством и контролем, и выбираешь правильный инструмент для задачи, а не просто берешь популярный ORM.

Почему используешь Spring JDBC в проекте? | PrepBro