Call Stored Procedures in PostgreSQL with Spring Boot 3

Spring Boot supports databases such as Oracle, MySQL, MongoDB, and PostgreSQL. It provides a call-stored procedure and function via a JDBC template or JPA(Java Persistence API).

It typically uses stored procedures and functions for batch processes or to implement specific business logic, such as managing logs or generating monthly reports.

Step-by-step guide for calling stored procedures and functions from PostgreSQL

1. Add dependencies in pom.xml

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>
<dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
     <optional>true</optional>
</dependency>

2. Add the PostgreSQL configuration connection to the application.properties file.

# ===============================
# = DATA SOURCE
# ===============================
spring.datasource.url=jdbc:postgresql://localhost:5432/demo
spring.datasource.username=admin
spring.datasource.password=password
spring.datasource.driver-class-name=org.postgresql.Driver

3. Create RESTful web services.

import jakarta.persistence.Column;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.Date;

@Getter
@Setter
public class UserProfilesBean {

    @Id
    @Column(name = "id")
    String id;

    @Column(name = "first_name")
    String firstName;

    @Column(name = "last_name")
    String lastName;

    @Column(name = "email")
    String email;

    @Column(name = "birth_date")
    Date birthDate;

    @Column(name = "sex")
    long sex;

    @Column(name = "create_by")
    String createBy;

    @Column(name = "create_date")
    LocalDateTime createDate;

    @Column(name = "update_by")
    String updateBy;

    @Column(name = "update_date")
    LocalDateTime updateDate;

}
import com.example.demo.bean.UserProfilesBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
public class UserProfilesController {
    @Autowired
    UserProfileServiceInf userProfileServiceInf;

    @GetMapping(path = "/user-profiles/{userid}", produces = "application/json")
    public ResponseEntity<Object> getUserprofile(@PathVariable String userid) {
        //call service
        List<UserProfilesBean> list = userProfileServiceInf.inqUserProfile(userid);
        return new ResponseEntity<>(list, HttpStatus.OK);
    }

}

Example call stored procedures using JDBC template

1. Create a service layer to get user profiles from the database using JdbcTemplate.

CREATE OR REPLACE PROCEDURE get_user_profiles(in user_id CHARACTER VARYING, out ref refcursor)
LANGUAGE plpgsql
AS $$
BEGIN
    -- Open a cursor and select all rows from user_profiles
    OPEN ref FOR 
 SELECT * FROM user_profiles
 where user_profiles.id::text = user_id;
END;
$$;
import com.example.demo.bean.UserProfilesBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

@Service
public class UserProfileServiceImpl implements UserProfileServiceInf {


    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public UserProfileServiceImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public List<UserProfilesBean> inqUserProfile(String userId) {
        List<UserProfilesBean> list = new ArrayList<>();
        Connection connection = null;
        try {
            connection = jdbcTemplate.getDataSource().getConnection();
            connection.setAutoCommit(false);
            CallableStatement call = connection.prepareCall("call get_user_profiles(?, ?)");
            call.setString(1, userId);
            call.registerOutParameter(2, Types.OTHER);
            call.execute();
            ResultSet rs = (ResultSet) call.getObject(2);
            while (rs.next()) {
                UserProfilesBean up = new UserProfilesBean();
                System.out.println("userId=" + rs.getObject("id"));
                up.setId(rs.getString("id"));
                up.setFirstName(rs.getString("first_name"));
                up.setLastName(rs.getString("last_name"));
                up.setEmail(rs.getString("email"));
                up.setBirthDate(rs.getDate("birth_date"));
                up.setSex(rs.getLong("sex"));
                up.setCreateBy(rs.getString("create_by"));
                up.setCreateDate(rs.getTimestamp("create_date") != null ? rs.getTimestamp("create_date").toLocalDateTime() : null);
                up.setUpdateBy(rs.getString("update_by"));
                up.setUpdateDate(rs.getTimestamp("update_date") != null ? rs.getTimestamp("update_date").toLocalDateTime() : null);
                list.add(up);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return list;
    }

}

2. Send the GET request using the URL with the user ID path parameter.
http://localhost:8080/user-profiles/18555d30-eba0-4077-a080-ce4eabe15bfd

[
  {
    "id": "18555d30-eba0-4077-a080-ce4eabe15bfd",
    "firstName": "John                                                                                                                                                  ",
    "lastName": "Doh                                                                                                                                                   ",
    "email": "demo@example.com                                                                                                                                                                                                                                                                                                                ",
    "birthDate": "1989-01-06",
    "sex": 1,
    "createBy": "f6fd6894-52ed-49b4-8a30-e6641d8408d3",
    "createDate": "2024-05-15T08:50:16.747656",
    "updateBy": null,
    "updateDate": null
  }
]

Example dynamic mapping column from the result set.

1. Create custom column annotation in package com.example.demo.utils.

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUN.TIME)
public @interface Column {
    String name();
    Class<?> type() default String.class; // Default to String type
}

2. Create a resultset mapper class.

import java.lang.reflect.Field;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import java.util.UUID;
import com.example.demo.utils;

public class ResultSetMapper {

    public static <T> T mapResultSetToObject(ResultSet rs, Class<T> outputClass) throws SQLException {
        T obj = null;
        try {
            obj = outputClass.getDeclaredConstructor().newInstance();
            Field[] fields = outputClass.getDeclaredFields();
            String columnName = null;
            for (Field field : fields) {
                if (field.isAnnotationPresent(Column.class)) {
                    Column column = field.getAnnotation(Column.class);
                    columnName = column.name();
                    Class<?> columnType = column.type();
                    Object value = getValueByType(rs, columnName, columnType);

                    if (null == value) continue;

                    field.setAccessible(true);
                    field.set(obj, value);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return obj;
    }

    private static Object getValueByType(ResultSet rs, String columnName, Class<?> columnType) throws SQLException {
        try {
            if (columnType == String.class) {
                String value = rs.getString(columnName);
                return null == value || value.isEmpty() ? null : value;
            } else if (columnType == UUID.class) {
                UUID value = (UUID) rs.getObject(columnName);
                return null == value ? null : String.valueOf(value);
            } else if (columnType == int.class || columnType == Integer.class) {
                return rs.getInt(columnName);
            } else if (columnType == long.class || columnType == Long.class) {
                return rs.getLong(columnName);
            } else if (columnType == double.class || columnType == Double.class) {
                return rs.getDouble(columnName);
            } else if (columnType == float.class || columnType == Float.class) {
                return rs.getFloat(columnName);
            } else if (columnType == boolean.class || columnType == Boolean.class) {
                return rs.getBoolean(columnName);
            } else if (columnType == Date.class) {
                return rs.getDate(columnName);
            } else if (columnType == Timestamp.class) {
                return rs.getTimestamp(columnName) != null ? rs.getTimestamp(columnName).toLocalDateTime() : null;
            } else {
                return rs.getObject(columnName);
            }
        } catch (Exception ex) {
            //skip column not found from stored procedure
            System.out.println("error message = " + ex.getMessage());
            System.out.println("columnName not found = " + columnName);
            return null;
        }
    }

}

3. Modify the bean Java class using a custom column annotation.

import com.example.demo.utils.Column;

@Getter
@Setter
public class UserProfilesBean {

    @Column(name = "id", type = UUID.class)
    String id;

    @Column(name = "first_name", type = String.class)
    String firstName;

    @Column(name = "last_name", type = String.class)
    String lastName;

    @Column(name = "email", type = String.class)
    String email;

    @Column(name = "birth_date", type = Date.class)
    Date birthDate;

    @Column(name = "sex", type = Long.class)
    long sex;

    @Column(name = "create_by", type = UUID.class)
    String createBy;

    @Column(name = "create_date", type = Timestamp.class)
    LocalDateTime createDate;

    @Column(name = "update_by", type = UUID.class)
    String updateBy;

    @Column(name = "update_date", type = Timestamp.class)
    LocalDateTime updateDate;

}

4. Modify the method in the UserProfileServiceImpl class.

public List<UserProfilesBean> inqUserProfile(String userId) {
    List<UserProfilesBean> list = new ArrayList<>();
    Connection connection = null;
    try {
        connection = jdbcTemplate.getDataSource().getConnection();
        connection.setAutoCommit(false);
        CallableStatement call = connection.prepareCall("call get_user_profiles(?, ?)");
        call.setString(1, userId);
        call.registerOutParameter(2, Types.OTHER);
        call.execute();
        ResultSet rs = (ResultSet) call.getObject(2);

        while (rs.next()) {
            UserProfilesBean obj = ResultSetMapper.mapResultSetToObject(rs, UserProfilesBean.class);
            list.add(obj);
        }

    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        try {
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    return list;
}

5. Send the GET request using the URL with the user ID path parameter.
http://localhost:8080/user-profiles/18555d30-eba0-4077-a080-ce4eabe15bfd

[
  {
    "id": "18555d30-eba0-4077-a080-ce4eabe15bfd",
    "firstName": "John",
    "lastName": "Doh",
    "email": "demo@example.com",
    "birthDate": "1989-01-06",
    "sex": 1,
    "createBy": "f6fd6894-52ed-49b4-8a30-e6641d8408d3",
    "createDate": "2024-05-15T08:50:16.747656",
    "updateBy": null,
    "updateDate": null
  }
]

Alternatively, the developer can send a parameter class to the result set, but the class from the stored procedure must match the Java class.
Such as the difference between "java.util.Date" and “java.sql.Date"

6. The result set returns a timestamp, but using a Java bean with LocalDateTime causes an error. The developer should decide which way is suitable for the project.

private static Object getValueByType(ResultSet rs, String columnName, Class<?> columnType){
    return rs.getObject(columnName, columnType);
}

Example call function using JDBC template

1. Add a calculateAge method to the user profile service.

//UserProfileServiceImpl.java
public int calculateAge(
        Date birthDate, Date dateTo) {
    String sql = "SELECT calculate_age(?,?)";
    Object[] params = {birthDate, dateTo};
    int[] types = {Types.DATE, Types.DATE};

    return jdbcTemplate.queryForObject(sql, params, types, Integer.class);
}

2. Add the calculate age method to the user profile controller.

@GetMapping(path = "/cal-age/{birthdate}", produces = "application/json")
public ResponseEntity<Object> calculateAge(@PathVariable String birthdate) {
    String format = "yyyy-MM-dd";
    int age = 0;
    if (birthdate == null) return null;

    SimpleDateFormat sdf = new SimpleDateFormat(format);
    try {
        Date birthdateInput = sdf.parse(birthdate);
        age = userProfileServiceInf.calculateAge(birthdateInput , new Date());
    } catch (ParseException e) {
        throw new Run.timeException(e);
    }

    return new ResponseEntity<>(age, HttpStatus.OK);
}

3. Send a GET request to the URL with the birthdate path parameter.
http://localhost:8080/cal-age/1986–06–01

36

Example call stored procedures using EntityManager.

1. Create a repository or service layer using EntityManager to call the stored procedure.

@PersistenceContext
private EntityManager entityManager;

@Transactional
public List<UserProfilesBean> inqUserProfile(String userId) {
    List<UserProfilesBean> list = new ArrayList<>();
    // Create stored procedure query
    StoredProcedureQuery storedProcedure = entityManager.createStoredProcedureQuery("get_user_profiles");

    // Set input parameters
    storedProcedure.registerStoredProcedureParameter(1, String.class, ParameterMode.IN);
    storedProcedure.setParameter(1, userId);

    // Register the output parameter
    storedProcedure.registerStoredProcedureParameter(2, void.class, ParameterMode.REF_CURSOR);

    // Execute the stored procedure
    storedProcedure.execute();

    // Get the output parameter result
    List<Object[]> results = storedProcedure.getResultList();

    list = results.stream()
            .map(row -> {
                UserProfilesBean up = new UserProfilesBean();
                up.setId(String.valueOf(row[0]));
                up.setFirstName((String) row[1]);
                up.setLastName((String) row[2]);
                up.setEmail((String) row[3]);
                up.setBirthDate((java.sql.Date) row[4]);
                up.setSex(new BigDecimal(row[5].toString()).longValue());
                up.setCreateBy(String.valueOf(row[6]));
                up.setCreateDate(row[7] != null ? ((java.sql.Timestamp) row[7]).toLocalDateTime() : null);
                up.setUpdateBy(String.valueOf(row[8]));
                up.setUpdateDate(row[9] != null ? ((java.sql.Timestamp) row[9]).toLocalDateTime() : null);
                return up;
            })
            .collect(Collectors.toList());

    return list;
}

2. Send the GET request using the URL with the user ID path parameter.
http://localhost:8080/user-profiles/18555d30-eba0-4077-a080-ce4eabe15bfd

[
  {
    "id": "18555d30-eba0-4077-a080-ce4eabe15bfd",
    "firstName": "John                                                                                                                                                  ",
    "lastName": "Doh                                                                                                                                                   ",
    "email": "demo@example.com                                                                                                                                                                                                                                                                                                                ",
    "birthDate": "1989-01-06",
    "sex": 1,
    "createBy": "f6fd6894-52ed-49b4-8a30-e6641d8408d3",
    "createDate": "2024-05-15T08:50:16.747656",
    "updateBy": "null",
    "updateDate": null
  }
]

Example dynamic mapping column from the list of object arrays.

1. Create a custom column annotation.

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUN.TIME)
public @interface Column {
    String name();
    Class<?> type() default String.class; // Default to String type
}

2. Edit Java bean.

import java.lang.reflect.Field;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import java.util.UUID;
import com.example.demo.utils;

public class ResultSetMapper {

    public static <T> T mapResultSetToObject(ResultSet rs, Class<T> outputClass) throws SQLException {
        T obj = null;
        try {
            obj = outputClass.getDeclaredConstructor().newInstance();
            Field[] fields = outputClass.getDeclaredFields();
            String columnName = null;
            for (Field field : fields) {
                if (field.isAnnotationPresent(Column.class)) {
                    Column column = field.getAnnotation(Column.class);
                    columnName = column.name();
                    Class<?> columnType = column.type();
                    Object value = getValueByType(rs, columnName, columnType);

                    if (null == value) continue;

                    field.setAccessible(true);
                    field.set(obj, value);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return obj;
    }

    private static Object getValueByType(ResultSet rs, String columnName, Class<?> columnType) throws SQLException {
        try {
            if (columnType == String.class) {
                String value = rs.getString(columnName);
                return null == value || value.isEmpty() ? null : value;
            } else if (columnType == UUID.class) {
                UUID value = (UUID) rs.getObject(columnName);
                return null == value ? null : String.valueOf(value);
            } else if (columnType == int.class || columnType == Integer.class) {
                return rs.getInt(columnName);
            } else if (columnType == long.class || columnType == Long.class) {
                return rs.getLong(columnName);
            } else if (columnType == double.class || columnType == Double.class) {
                return rs.getDouble(columnName);
            } else if (columnType == float.class || columnType == Float.class) {
                return rs.getFloat(columnName);
            } else if (columnType == boolean.class || columnType == Boolean.class) {
                return rs.getBoolean(columnName);
            } else if (columnType == Date.class) {
                return rs.getDate(columnName);
            } else if (columnType == Timestamp.class) {
                return rs.getTimestamp(columnName) != null ? rs.getTimestamp(columnName).toLocalDateTime() : null;
            } else {
                return rs.getObject(columnName);
            }
        } catch (Exception ex) {
            //skip column not found from stored procedure
            System.out.println("error message = " + ex.getMessage());
            System.out.println("columnName not found = " + columnName);
            return null;
        }
    }

}

3. Create a row mapper.

import com.example.demo.utils.Column;

@Getter
@Setter
public class UserProfilesBean {

    @Column(name = "id", type = UUID.class)
    String id;

    @Column(name = "first_name", type = String.class)
    String firstName;

    @Column(name = "last_name", type = String.class)
    String lastName;

    @Column(name = "email", type = String.class)
    String email;

    @Column(name = "birth_date", type = Date.class)
    Date birthDate;

    @Column(name = "sex", type = Long.class)
    long sex;

    @Column(name = "create_by", type = UUID.class)
    String createBy;

    @Column(name = "create_date", type = Timestamp.class)
    LocalDateTime createDate;

    @Column(name = "update_by", type = UUID.class)
    String updateBy;

    @Column(name = "update_date", type = Timestamp.class)
    LocalDateTime updateDate;

}

4. Edit service.

public List<UserProfilesBean> inqUserProfile(String userId) {
    List<UserProfilesBean> list = new ArrayList<>();
    Connection connection = null;
    try {
        connection = jdbcTemplate.getDataSource().getConnection();
        connection.setAutoCommit(false);
        CallableStatement call = connection.prepareCall("call get_user_profiles(?, ?)");
        call.setString(1, userId);
        call.registerOutParameter(2, Types.OTHER);
        call.execute();
        ResultSet rs = (ResultSet) call.getObject(2);

        while (rs.next()) {
            UserProfilesBean obj = ResultSetMapper.mapResultSetToObject(rs, UserProfilesBean.class);
            list.add(obj);
        }

    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        try {
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    return list;
}

5. Send the GET request using the URL with the user ID path parameter.
http://localhost:8080/user-profiles/18555d30-eba0-4077-a080-ce4eabe15bfd

[
  {
    "id": "18555d30-eba0-4077-a080-ce4eabe15bfd",
    "firstName": "John",
    "lastName": "Doh",
    "email": "demo@example.com",
    "birthDate": "1989-01-06",
    "sex": 1,
    "createBy": "f6fd6894-52ed-49b4-8a30-e6641d8408d3",
    "createDate": "2024-05-15T08:50:16.747656",
    "updateBy": null,
    "updateDate": null
  }
]

Conclusion

The developer can modify the mapping class to suit the project’s business requirements.

Example call function using EntityManager

1. Modify the age calculation method in the user profile service.

@PersistenceContext
private EntityManager entityManager;
    
public int calculateAge(Date birthDate, Date dateTo) {
    Query query = entityManager.createNativeQuery("SELECT calculate_age(?1, ?2)");
    query.setParameter(1, birthDate);
    query.setParameter(2, dateTo);
    return (Integer) query.getSingleResult();
}

2. Send a GET request to the URL with the birthdate path parameter.
http://localhost:8080/cal-age/1994–06–01

29

Example call stored procedures using JPA repository

1. Create a repository using the JPA repository for the stored procedure call.

//UserProfileRepository.java
import com.example.demo.bean.UserProfile;
import com.example.demo.bean.UserProfilesBean;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserProfileRepository extends JpaRepository<UserProfilesBean, Long> {

    @Procedure(name = "get_user_profiles" ,procedureName = "get_user_profiles",refCursor = true)
    List<UserProfilesBean> getUserProfiles(@Param("userId") String userId);
}

stored procedure must return all columns in UserProfilesBean; otherwise, cause an error column not found.

2. Modify UserProfilesBean by adding the Entity annotation.

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.Date;

@Getter
@Setter
@Entity
public class UserProfilesBean {

    @Id
    @Column(name = "id")
    String id;

    @Column(name = "first_name")
    String firstName;

    @Column(name = "last_name")
    String lastName;

    @Column(name = "email")
    String email;

    @Column(name = "birth_date")
    Date birthDate;

    @Column(name = "sex")
    long sex;

    @Column(name = "create_by")
    String createBy;

    @Column(name = "create_date")
    LocalDateTime createDate;

    @Column(name = "update_by")
    String updateBy;

    @Column(name = "update_date")
    LocalDateTime updateDate;


}

3. Send the GET request using the URL with the user ID path parameter.
http://localhost:8080/user-profiles/18555d30-eba0-4077-a080-ce4eabe15bfd

[
  {
    "id": "18555d30-eba0-4077-a080-ce4eabe15bfd",
    "firstName": "John",
    "lastName": "Doh",
    "email": "demo@example.com",
    "birthDate": "1989-01-05T17:00:00.000+00:00",
    "sex": 1,
    "createBy": "f6fd6894-52ed-49b4-8a30-e6641d8408d3",
    "createDate": "2024-05-15T08:50:16.747656",
    "updateBy": null,
    "updateDate": null
  }
]

For example, call stored procedures using the JPA repository, output more than one parameter implemented with the NamedStoredProcedureQuery annotation.

CREATE OR REPLACE procedure get_user_details(
    IN user_id CHARACTER VARYING,
    OUT out_first_name CHARACTER VARYING,
    OUT out_last_name CHARACTER VARYING
)
LANGUAGE plpgsql
AS $$
BEGIN

    SELECT first_name, last_name INTO out_first_name, out_last_name
    FROM user_profiles
    WHERE id::text = user_id;

    RETURN;
END;
$$;

1. Modify UserProfilesBean by adding NamedStoredProcedureQuery annotation.

//UserProfilesBean.java
@NamedStoredProcedureQuery(name = "get_user_details",
procedureName = "get_user_details", parameters = {
@StoredProcedureParameter(mode = ParameterMode.IN, name = "user_id", type = String.class),
@StoredProcedureParameter(mode = ParameterMode.OUT, name = "out_first_name", type = String.class),
@StoredProcedureParameter(mode = ParameterMode.OUT, name = "out_last_name", type = String.class)})
public class UserProfilesBean {...}

2. Modify UserProfileRepository by adding a method.

//UserProfileRepository.java
@Procedure(name = "get_user_details")
Map<String, Object> getUserProfilesName(@Param("user_id") String userId);

3. Add a new web service method.

//UserProfilesController.java
@GetMapping(path = "/user-details/{userid}", produces = "application/json")
public ResponseEntity<Object> getUserDetail(@PathVariable String userid) throws JsonProcessingException {
    //call service
    String result = userProfileServiceInf.getUserDetail(userid);
    return new ResponseEntity<>(result, HttpStatus.OK);
}

4. Send the GET request URL using the user ID path parameter.
http://localhost:8080/user-profiles/18555d30-eba0-4077-a080-ce4eabe15bfd

{
  "out_last_name": "Doh",
  "out_first_name": "John"
}

Example call function using JPA repository

1. Modify the repository by adding a ‘calculate age’ method.

//UserProfileRepository.java
@Query(value = "SELECT calculate_age(:birthDate, :dateTo)", nativeQuery = true)
Integer calculateAge(@Param("birthDate") Date birthDate, @Param("dateTo") Date dateTo);

2. Modify the service by adding a calculateAge method.

public int calculateAge(
        Date birthDate, Date dateTo) {
    return userProfileRepository.calculateAge(birthDate, dateTo);
}

3. Send the GET request URL with the birthdate and current date path parameters.
http://localhost:8080/cal-age/1994-06-01

29

Call a stored procedure using the StoredProcedure class from the Spring Framework

Spring Boot provides support for the Stored Procedure class, allowing you to call stored procedures from the database.

1. Create an ObjectMapper class to map results from the ref cursor to the Java bean.

import org.springframework.jdbc.core.RowMapper;
import java.lang.reflect.Field;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import java.util.UUID;

public record ObjectMapper(Class theClass) implements RowMapper {

    private static Object getValueByType(ResultSet rs, String columnName, Class<?> columnType) throws SQLException {

        if (columnType == Date.class) {
            columnType = java.sql.Date.class;
        }

        return rs.getObject(columnName, columnType);
    }

    @Override
    public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
        Object obj = null;
        try {
            obj = theClass.newInstance();
            Field[] fields = obj.getClass().getDeclaredFields();
            String columnName = null;
            for (Field field : fields) {
                if (field.isAnnotationPresent(Column.class)) {
                    Column column = field.getAnnotation(Column.class);
                    columnName = column.name();
                    Class<?> columnType = column.type();
                    Object value = getValueByType(rs, columnName, columnType);

                    if (null == value) continue;

                    if (value instanceof UUID) {
                        value = String.valueOf(value);
                    } else if (value instanceof Timestamp) {
                        value = ((Timestamp) value).toLocalDateTime();
                    }

                    field.setAccessible(true);
                    field.set(obj, value);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return obj;
    }

}

2. Create a Java bean to map results from the ref cursor.

import com.example.demo.utils.Column;
import com.example.demo.utils.ColumnByIndex;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.sql.ResultSet;
import java.sql.Struct;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.LocalDateTime;
import java.util.*;

@Getter
@Setter
public class UserProfilesBean {

    @Column(name = "id", type = UUID.class)
    String id;

    @Column(name = "first_name", type = String.class)
    String firstName;

    @Column(name = "last_name", type = String.class)
    String lastName;

    @Column(name = "email", type = String.class)
    String email;

    @Column(name = "birth_date", type = Date.class)
    Date birthDate;

    @Column(name = "sex", type = Integer.class)
    int sex;

    @Column(name = "create_by", type = UUID.class)
    String createBy;

    @Column(name = "create_date", type = Timestamp.class)
    LocalDateTime createDate;

    @Column(name = "update_by", type = UUID.class)
    String updateBy;

    @Column(name = "update_date", type = Timestamp.class)
    LocalDateTime updateDate;

}

3. Create a method in the service class for calling the stored procedure.

//UserProfileServiceImpl.java

@Transactional
public List<UserProfilesBean> inqUserProfile(String userId) {
    ObjectStoredProcedure proc = new ObjectStoredProcedure(jdbcTemplate, UserProfilesBean.class);
    Map<String, Object> result = proc.execute(userId);
    return (List<UserProfilesBean>) result.get("result");
}

private static class ObjectStoredProcedure<T> extends StoredProcedure {
    private static final String SP_NAME = "call get_user_profiles(?, ?)";

    public ObjectStoredProcedure(JdbcTemplate jdbcTemplate, Class<T> classz) {
        //the developer can modify for dynamic stored procedure and parameter.
        super(jdbcTemplate, SP_NAME);
        setFunction(false);
        setSqlReadyForUse(true);
        setSql(SP_NAME);
        declareParameter(new SqlParameter("p_userid", Types.VARCHAR));
        declareParameter(new SqlOutParameter("result", Types.REF_CURSOR, new ObjectMapper(classz)));
        compile();
    }

    public Map<String, Object> execute(int userId) {
        HashMap input = new HashMap();
        input.put("p_userid", userId);
        return super.execute(input);
    }
}

4. In ObjectStoredProcedure method.
setSqlReadyForUse(true) and setSql(“call get_user_profiles(?, ?)”)
because the bracket “{}” in the PostgreSQL database, which is auto-generated from the StoredProcedure class, causes an error: “badsqlgrammarexception”.
But it works perfectly in the Oracle database.

Generate from StoredProcedure class "{call get_user_profiles(?, ?)}" 
must change to "call get_user_profiles(?, ?)"

Recommend

The developer should modify the ObjectStoredProcedure class for dynamic stored procedures and parameters that make an application
easy to maintain and reduce redundant code.

5. Send the GET request using the URL with the user ID path parameter.
http://localhost:8080/user-profiles/18555d30-eba0-4077-a080-ce4eabe15bfd

[
  {
    "id": "18555d30-eba0-4077-a080-ce4eabe15bfd",
    "firstName": "John",
    "lastName": "Doh",
    "email": "demo@example.com",
    "birthDate": "1989-01-06",
    "sex": 1,
    "createBy": "f6fd6894-52ed-49b4-8a30-e6641d8408d3",
    "createDate": "2024-05-15T08:50:16.747656",
    "updateBy": null,
    "updateDate": null
  }
]

Finally

Spring Boot provides various features for calling stored procedures and functions. The developer should learn to limit each feature in Spring Boot to prevent problems.

Leave a Comment

Your email address will not be published. Required fields are marked *