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

Как вызвать хранимые процедуры через JDBC

2.0 Middle🔥 121 комментариев
#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Вызов хранимых процедур через JDBC

Краткий ответ

Для вызова хранимой процедуры через JDBC используется интерфейс CallableStatement, который расширает PreparedStatement и поддерживает параметры вывода (OUT) и возвращаемые значения.

Синтаксис

// Получить CallableStatement
CallableStatement cstmt = connection.prepareCall("{call procedure_name(?, ?, ?)}");

// Установить параметры входа (IN)
cstmt.setInt(1, value1);
cstmt.setString(2, value2);

// Зарегистрировать параметры выхода (OUT)
cstmt.registerOutParameter(3, Types.INTEGER);

// Выполнить процедуру
cstmt.execute();

// Получить результаты
int result = cstmt.getInt(3);

Полный пример 1: Простая процедура без параметров выхода

Хранимая процедура (PostgreSQL)

CREATE PROCEDURE insert_user(
    p_name VARCHAR,
    p_email VARCHAR
)
LANGUAGE plpgsql
AS $$
BEGIN
    INSERT INTO users (name, email) VALUES (p_name, p_email);
    COMMIT;
END;
$$;

Java код

@Component
public class UserDAO {
    
    @Autowired
    private DataSource dataSource;
    
    public void callInsertUserProcedure(String name, String email) {
        String sql = "{call insert_user(?, ?)}";
        
        try (Connection conn = dataSource.getConnection();
             CallableStatement cstmt = conn.prepareCall(sql)) {
            
            // Установка параметров входа
            cstmt.setString(1, name);
            cstmt.setString(2, email);
            
            // Выполнение процедуры
            cstmt.execute();
            
        } catch (SQLException e) {
            throw new DatabaseException("Ошибка при вызове процедуры", e);
        }
    }
}

Полный пример 2: Процедура с параметром выхода (OUT)

Хранимая процедура (PostgreSQL)

CREATE PROCEDURE get_user_count(
    OUT p_count INTEGER
)
LANGUAGE plpgsql
AS $$
BEGIN
    SELECT COUNT(*) INTO p_count FROM users;
END;
$$;

Java код

public int callGetUserCountProcedure() {
    String sql = "{call get_user_count(?)}";
    
    try (Connection conn = dataSource.getConnection();
         CallableStatement cstmt = conn.prepareCall(sql)) {
        
        // Зарегистрировать параметр выхода
        cstmt.registerOutParameter(1, Types.INTEGER);
        
        // Выполнить процедуру
        cstmt.execute();
        
        // Получить результат
        return cstmt.getInt(1);
        
    } catch (SQLException e) {
        throw new DatabaseException("Ошибка при вызове процедуры", e);
    }
}

Полный пример 3: Смешанные IN и OUT параметры

Хранимая процедура (SQL Server)

CREATE PROCEDURE calculate_discount(
    @p_order_amount DECIMAL(10,2),
    @p_customer_level INT,
    @p_discount_percent OUTPUT,
    @p_final_amount OUTPUT DECIMAL(10,2)
)
AS
BEGIN
    DECLARE @discount DECIMAL(5,2)
    
    -- Определение скидки на основе уровня клиента
    IF @p_customer_level = 1
        SET @discount = 5.0
    ELSE IF @p_customer_level = 2
        SET @discount = 10.0
    ELSE
        SET @discount = 15.0
    
    -- Установка выходных параметров
    SET @p_discount_percent = @discount
    SET @p_final_amount = @p_order_amount * (1 - @discount / 100)
END;

Java код

public class DiscountCalculator {
    
    @Autowired
    private DataSource dataSource;
    
    public DiscountResult calculateDiscount(BigDecimal orderAmount, int customerLevel) {
        String sql = "{call calculate_discount(?, ?, ?, ?)}";
        
        try (Connection conn = dataSource.getConnection();
             CallableStatement cstmt = conn.prepareCall(sql)) {
            
            // Параметры входа (IN)
            cstmt.setBigDecimal(1, orderAmount);
            cstmt.setInt(2, customerLevel);
            
            // Параметры выхода (OUT)
            cstmt.registerOutParameter(3, Types.DECIMAL);
            cstmt.registerOutParameter(4, Types.DECIMAL);
            
            // Выполнение
            cstmt.execute();
            
            // Получение результатов
            BigDecimal discountPercent = cstmt.getBigDecimal(3);
            BigDecimal finalAmount = cstmt.getBigDecimal(4);
            
            return new DiscountResult(discountPercent, finalAmount);
            
        } catch (SQLException e) {
            throw new DatabaseException("Ошибка при вызове процедуры расчета скидки", e);
        }
    }
}

public record DiscountResult(BigDecimal discountPercent, BigDecimal finalAmount) {}

Полный пример 4: Процедура с возвращаемым значением

Хранимая процедура (MySQL)

DELIMITER //

CREATE PROCEDURE update_user_status(
    IN p_user_id INT,
    IN p_status VARCHAR(20),
    OUT p_success INT
)
READS SQL DATA
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        SET p_success = 0;
    END;
    
    UPDATE users SET status = p_status WHERE id = p_user_id;
    SET p_success = 1;
END//

DELIMITER ;

Java код

public boolean updateUserStatus(int userId, String status) {
    String sql = "{call update_user_status(?, ?, ?)}";
    
    try (Connection conn = dataSource.getConnection();
         CallableStatement cstmt = conn.prepareCall(sql)) {
        
        // IN параметры
        cstmt.setInt(1, userId);
        cstmt.setString(2, status);
        
        // OUT параметр для успеха/ошибки
        cstmt.registerOutParameter(3, Types.INTEGER);
        
        cstmt.execute();
        
        int success = cstmt.getInt(3);
        return success == 1;
        
    } catch (SQLException e) {
        throw new DatabaseException("Ошибка при обновлении статуса пользователя", e);
    }
}

Полный пример 5: Процедура с ResultSet

Хранимая процедура (Oracle)

CREATE PROCEDURE get_users_by_status(
    p_status IN VARCHAR2,
    p_cursor OUT SYS_REFCURSOR
)
IS
BEGIN
    OPEN p_cursor FOR
        SELECT id, name, email, status
        FROM users
        WHERE status = p_status;
END;
/

Java код

public List<User> getUsersByStatus(String status) {
    String sql = "{call get_users_by_status(?, ?)}";
    List<User> users = new ArrayList<>();
    
    try (Connection conn = dataSource.getConnection();
         CallableStatement cstmt = conn.prepareCall(sql)) {
        
        // IN параметр
        cstmt.setString(1, status);
        
        // OUT параметр - результирующий набор
        cstmt.registerOutParameter(2, OracleTypes.CURSOR);
        
        cstmt.execute();
        
        // Получение ResultSet из OUT параметра
        ResultSet rs = (ResultSet) cstmt.getObject(2);
        
        while (rs.next()) {
            User user = new User();
            user.setId(rs.getInt("id"));
            user.setName(rs.getString("name"));
            user.setEmail(rs.getString("email"));
            user.setStatus(rs.getString("status"));
            users.add(user);
        }
        
        return users;
        
    } catch (SQLException e) {
        throw new DatabaseException("Ошибка при получении пользователей", e);
    }
}

Типы параметров

// Регистрация разных типов OUT параметров
cstmt.registerOutParameter(1, Types.INTEGER);        // INT
cstmt.registerOutParameter(2, Types.VARCHAR);        // VARCHAR
cstmt.registerOutParameter(3, Types.DECIMAL);        // DECIMAL
cstmt.registerOutParameter(4, Types.DATE);           // DATE
cstmt.registerOutParameter(5, Types.TIMESTAMP);      // TIMESTAMP
cstmt.registerOutParameter(6, Types.BOOLEAN);        // BOOLEAN
cstmt.registerOutParameter(7, Types.ARRAY);          // ARRAY
cstmt.registerOutParameter(8, Types.OTHER);          // CURSOR (Oracle)

Обработка исключений

public void callProcedureWithErrorHandling(String param) {
    String sql = "{call my_procedure(?)}";
    
    try (Connection conn = dataSource.getConnection();
         CallableStatement cstmt = conn.prepareCall(sql)) {
        
        cstmt.setString(1, param);
        cstmt.execute();
        
    } catch (SQLException e) {
        if (e.getErrorCode() == 1234) {
            // Специфичная обработка
            throw new CustomBusinessException("Ошибка бизнес-логики", e);
        } else {
            // Общая обработка
            throw new DatabaseException("Ошибка БД при вызове процедуры", e);
        }
    }
}

Использование Spring JdbcTemplate (более удобно)

@Service
public class UserService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    public int getUserCount() {
        return jdbcTemplate.execute(
            (CallableStatement cs) -> {
                cs.registerOutParameter(1, Types.INTEGER);
                cs.execute();
                return cs.getInt(1);
            },
            false,
            "{call get_user_count(?)}"
        );
    }
    
    public List<User> getUsersByStatus(String status) {
        return jdbcTemplate.query(
            "{call get_users_by_status(?)}",
            ps -> ps.setString(1, status),
            (ResultSet rs, int rowNum) -> {
                User user = new User();
                user.setId(rs.getInt("id"));
                user.setName(rs.getString("name"));
                return user;
            }
        );
    }
}

Spring Data JPA (самый удобный способ)

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
    
    @Procedure(name = "get_user_count")
    Integer getUserCount();
    
    @Procedure(name = "get_users_by_status")
    List<User> getUsersByStatus(String status);
}

// Использование
@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    public int getCount() {
        return userRepository.getUserCount();
    }
}

Синтаксис вызова для разных БД

// PostgreSQL
String sql = "{call procedure_name(?, ?)}";

// MySQL
String sql = "{call procedure_name(?, ?)}";

// Oracle
String sql = "{? = call procedure_name(?, ?)}";  // Если есть RETURN

// SQL Server
String sql = "{call procedure_name(?, ?)}";

// DB2
String sql = "{call schema.procedure_name(?, ?)}";

Best Practices

  1. Используй try-with-resources для автоматического закрытия ресурсов
  2. Отлавливай SQLException и преобразуй в бизнес-исключения
  3. Логируй SQL-запросы для отладки
  4. Используй Spring JdbcTemplate или Spring Data JPA вместо сырого JDBC
  5. Параметризируй все значения для защиты от SQL-инъекций
  6. Документируй назначение процедур и их параметры
  7. Тестируй процедуры с помощью юнит-тестов

Вывод

CallableStatement позволяет вызывать хранимые процедуры из Java с поддержкой входных, выходных параметров и результирующих наборов. Для удобства рекомендуется использовать Spring JdbcTemplate или Spring Data JPA вместо прямого JDBC.

Как вызвать хранимые процедуры через JDBC | PrepBro