Пакет org.springframework.jdbc.object содержит классы, которые позволяют получить доступ к базе данных более объектно-ориентированным способом. Например, можно выполнять запросы и получать результаты в виде списка, содержащего бизнес-объекты с данными реляционных колонок, сопоставленными со свойствами бизнес-объекта. Также можно запускать хранимые процедуры и выполнять инструкции обновления, удаления и вставки.

Многие разработчики Spring считают, что различные классы операций РСУБД, описанные ниже (за исключением класса StoredProcedure ), чаще всего можно заменить прямыми вызовами JdbcTemplate. Зачастую проще написать метод DAO, который вызывает метод из JdbcTemplate напрямую (в отличие от инкапсуляции запроса в полноценный класс).

Однако, если вы находите значительно полезным использование классов операций РСУБД, вам следует продолжать использовать эти классы.

Основные сведения о SqlQuery

SqlQuery – это многократно используемый, потокобезопасный класс, который инкапсулирует SQL-запрос. Подклассы должны реализовать метод newRowMapper(..) для предоставления экземпляра RowMapper, который может создавать один объект для каждой строки, полученной в результате обхода ResultSet, который создается во время выполнения запроса. Класс SqlQuery редко используется напрямую, поскольку подкласс MappingSqlQuery предоставляет гораздо более удобную реализацию для отображения строк на классы Java. Другими реализациями, расширяющими SqlQuery, являются MappingSqlQueryWithParameters и UpdatableSqlQuery.

Использование MappingSqlQuery

MappingSqlQuery – это многократно используемый запрос, в котором конкретные подклассы должны реализовать абстрактный метод mapRow(..) для преобразования каждой строки предоставляемого ResultSet в объект указанного типа. В следующем примере показан кастомный запрос, который сопоставляет данные из отношения t_actor с экземпляром класса Actor:

Java
public class ActorMappingQuery extends MappingSqlQuery<Actor> {
    public ActorMappingQuery(DataSource ds) {
        super(ds, "select id, first_name, last_name from t_actor where id = ?");
        declareParameter(new SqlParameter("id", Types.INTEGER));
        compile();
    }
    @Override
    protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
        Actor actor = new Actor();
        actor.setId(rs.getLong("id"));
        actor.setFirstName(rs.getString("first_name"));
        actor.setLastName(rs.getString("last_name"));
        return actor;
    }
}
Kotlin
class ActorMappingQuery(ds: DataSource) : MappingSqlQuery<Actor>(ds, "select id, first_name, last_name from t_actor where id = ?") {
    init {
        declareParameter(SqlParameter("id", Types.INTEGER))
        compile()
    }
    override fun mapRow(rs: ResultSet, rowNumber: Int) = Actor(
            rs.getLong("id"),
            rs.getString("first_name"),
            rs.getString("last_name")
    )
}

Класс расширяет MappingSqlQuery, параметризованный типом Actor. Конструктор для этого кастомного запроса принимает DataSource в качестве единственного параметра. В этом конструкторе можно вызвать конструктор суперкласса с DataSource и SQL, который должен быть выполнен, чтобы получить строки по этому запросу. Этот SQL используется для создания PreparedStatement, поэтому он может содержать плейсхолдеры для любых параметров, которые будут переданы во время выполнения. Необходимо объявить каждый параметр с помощью метода declareParameter, передав ему SqlParameter. SqlParameter принимает имя и тип JDBC, определенный в java.sql.Types. После определения всех параметров можно вызвать метод compile(), чтобы подготовить и впоследствии выполнить инструкцию. После компиляции этот класс является потокобезопасным, поэтому, пока эти экземпляры создаются при инициализации DAO, их можно хранить как переменные экземпляра и использовать повторно. В следующем примере показано, как определить такой класс:

Java
private ActorMappingQuery actorMappingQuery;
@Autowired
public void setDataSource(DataSource dataSource) {
    this.actorMappingQuery = new ActorMappingQuery(dataSource);
}
public Customer getCustomer(Long id) {
    return actorMappingQuery.findObject(id);
}
Kotlin
private val actorMappingQuery = ActorMappingQuery(dataSource)
fun getCustomer(id: Long) = actorMappingQuery.findObject(id)

Метод в предыдущем примере получает клиента с id, переданным в качестве единственного параметра. Поскольку нам нужно вернуть только один объект, мы вызываем вспомогательный метод findObject с id в качестве параметра. Если бы вместо этого был запрос, который возвращает список объектов и принимает дополнительные параметры, мы бы использовали один из методов execute, который принимает массив значений параметров, переданных в качестве аргументов переменной длины. В следующем примере показан такой метод:

Java
public List<Actor> searchForActors(int age, String namePattern) {
    List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
    return actors;
}
Kotlin
fun searchForActors(age: Int, namePattern: String) =
            actorSearchMappingQuery.execute(age, namePattern)

Использование SqlUpdate

Класс SqlUpdate инкапсулирует обновление SQL. Как и запрос, объект обновления является многоразовым, и, как и все классы RdbmsOperation, обновление может иметь параметры и определяется на языке SQL. Этот класс предоставляет ряд методов update(..), аналогичных методам execute(..) объектов-запросов. Класс SqlUpdate является конкретным. Его можно разбить на подклассы – например, чтобы добавить кастомный метод обновления. Однако вам не обязательно разбивать на подклассы класс SqlUpdate, поскольку его можно легко параметризовать, задав SQL и объявив параметры. В следующем примере создается пользовательский метод обновления с именем execute:

Java
import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;
public class UpdateCreditRating extends SqlUpdate {
    public UpdateCreditRating(DataSource ds) {
        setDataSource(ds);
        setSql("update customer set credit_rating = ? where id = ?");
        declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
        declareParameter(new SqlParameter("id", Types.NUMERIC));
        compile();
    }
    /**
     * @param id для обновляемого Customer
     * @param rating новое значение кредитного рейтинга
     * @return количество обновленных строк
     */
    public int execute(int id, int rating) {
        return update(rating, id);
    }
}
Kotlin
import java.sql.Types
import javax.sql.DataSource
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.SqlUpdate
class UpdateCreditRating(ds: DataSource) : SqlUpdate() {
    init {
        setDataSource(ds)
        sql = "update customer set credit_rating = ? where id = ?"
        declareParameter(SqlParameter("creditRating", Types.NUMERIC))
        declareParameter(SqlParameter("id", Types.NUMERIC))
        compile()
    }
    /**
     * @param id для обновляемого Customer
     * @param rating новое значение кредитного рейтинга
     * @return количество обновленных строк
     */
    fun execute(id: Int, rating: Int): Int {
        return update(rating, id)
    }
}

Использование StoredProcedure

Класс StoredProcedure является abstract суперклассом для объектных абстракций хранимых процедур РСУБД.

Наследуемое свойство sql – это имя хранимой процедуры в РСУБД.

Чтобы определить параметр для класса StoredProcedure, можно использовать SqlParameter или один из его подклассов. Необходимо задать имя параметра и тип SQL в конструкторе, как показано в следующем фрагменте кода:

Java
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
Kotlin
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),

Тип SQL задается с помощью констант java.sql.Types.

Первая строка (с SqlParameter) объявляет IN параметр. Можно использовать IN параметры как для вызовов хранимых процедур, так и для запросов с помощью SqlQuery и его подклассов .

Вторая строка (с SqlOutParameter) объявляет out параметр, который будет использоваться в вызове хранимой процедуры. Существует также SqlInOutParameter для InOut параметров (параметры, которые предоставляют процедуре значение in, а также возвращают значение).

Для in параметров, помимо имени и типа SQL, можно задать шкалу для числовых данных или имя типа для кастомно созданных типов базы данных. Для out параметров можно указать RowMapper, чтобы производить обработку отображения строк, возвращаемых из курсора REF. Другой вариант – задать SqlReturnType, который позволяет определить индивидуальную обработку возвращаемых значений.

В следующем примере простого DAO используется StoredProcedure для вызова функции(sysdate()), которая поставляется с любой базой данных Oracle. Чтобы использовать функциональность хранимой процедуры, необходимо создать класс, расширяющий StoredProcedure. В этом примере класс StoredProcedure является внутренним классом. Однако если нужно повторно использовать StoredProcedure, то можете объявить ее как класс верхнего уровня. В этом примере нет входных параметров, но выходной параметр объявлен как тип даты с помощью класса SqlOutParameter. Метод execute() запускает процедуру и извлекает возвращаемую дату из результирующего Map. Результирующий Map имеет запись для каждого объявленного выходного параметра (в данном случае только одного), используя имя параметра в качестве ключа. В следующем листинге показан наш кастомный класс StoredProcedure:

Java
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class StoredProcedureDao {
    private GetSysdateProcedure getSysdate;
    @Autowired
    public void init(DataSource dataSource) {
        this.getSysdate = new GetSysdateProcedure(dataSource);
    }
    public Date getSysdate() {
        return getSysdate.execute();
    }
    private class GetSysdateProcedure extends StoredProcedure {
        private static final String SQL = "sysdate";
        public GetSysdateProcedure(DataSource dataSource) {
            setDataSource(dataSource);
            setFunction(true);
            setSql(SQL);
            declareParameter(new SqlOutParameter("date", Types.DATE));
            compile();
        }
        public Date execute() {
            // Строка "sysdate" не имеет входных параметров, поэтому выдается пустой Map...
            Map<String, Object> results = execute(new HashMap<String, Object>());
            Date sysdate = (Date) results.get("date");
            return sysdate;
        }
    }
}
Kotlin
import java.sql.Types
import java.util.Date
import java.util.Map
import javax.sql.DataSource
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.object.StoredProcedure
class StoredProcedureDao(dataSource: DataSource) {
    private val SQL = "sysdate"
    private val getSysdate = GetSysdateProcedure(dataSource)
    val sysdate: Date
        get() = getSysdate.execute()
    private inner class GetSysdateProcedure(dataSource: DataSource) : StoredProcedure() {
        init {
            setDataSource(dataSource)
            isFunction = true
            sql = SQL
            declareParameter(SqlOutParameter("date", Types.DATE))
            compile()
        }
        fun execute(): Date {
            // Строка "sysdate" не имеет входных параметров, поэтому выдается пустой Map...
            val results = execute(mutableMapOf<String, Any>())
            return results["date"] as Date
        }
    }
}

Следующий пример StoredProcedure имеет два выходных параметра (в данном случае REF-курсоры из Oracle):

Java
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class TitlesAndGenresStoredProcedure extends StoredProcedure {
    private static final String SPROC_NAME = "AllTitlesAndGenres";
    public TitlesAndGenresStoredProcedure(DataSource dataSource) {
        super(dataSource, SPROC_NAME);
        declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
        declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
        compile();
    }
    public Map<String, Object> execute() {
        // опять же, у этой хранимой процедуры нет входных параметров, поэтому выдается пустой Map
        return super.execute(new HashMap<String, Object>());
    }
}
Kotlin
import java.util.HashMap
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.object.StoredProcedure
class TitlesAndGenresStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) {
    companion object {
        private const val SPROC_NAME = "AllTitlesAndGenres"
    }
    init {
        declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
        declareParameter(SqlOutParameter("genres", OracleTypes.CURSOR, GenreMapper()))
        compile()
    }
    fun execute(): Map<String, Any> {
        // опять же, у этой хранимой процедуры нет входных параметров, поэтому выдается пустой Map
        return super.execute(HashMap<String, Any>())
    }
}

Обратите внимание, как перегруженные варианты метода declareParameter(..), которые были использованы в конструкторе TitlesAndGenresStoredProcedure, передаются экземплярам реализации RowMapper. Это очень удобный и эффективный способ повторного использования существующей функциональности. Следующие два примера содержат код для двух реализаций RowMapper.

Класс TitleMapper сопоставляет ResultSet с объектом предметной области Title для каждой строки в предоставленном ResultSet следующим образом:

Java
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Title;
import org.springframework.jdbc.core.RowMapper;
public final class TitleMapper implements RowMapper<Title> {
    public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
        Title title = new Title();
        title.setId(rs.getLong("id"));
        title.setName(rs.getString("name"));
        return title;
    }
}
Kotlin
import java.sql.ResultSet
import com.foo.domain.Title
import org.springframework.jdbc.core.RowMapper
class TitleMapper : RowMapper<Title> {
    override fun mapRow(rs: ResultSet, rowNum: Int) =
            Title(rs.getLong("id"), rs.getString("name"))
}

Класс GenreMapper сопоставляет ResultSet с объектом предметной области Genre для каждой строки в предоставленном ResultSet следующим образом:

Java
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Genre;
import org.springframework.jdbc.core.RowMapper;
public final class GenreMapper implements RowMapper<Genre> {
    public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Genre(rs.getString("name"));
    }
}
Kotlin
import java.sql.ResultSet
import com.foo.domain.Genre
import org.springframework.jdbc.core.RowMapper
class GenreMapper : RowMapper<Genre> {
    override fun mapRow(rs: ResultSet, rowNum: Int): Genre {
        return Genre(rs.getString("name"))
    }
}

Чтобы передать параметры в хранимую процедуру, которая содержит один или несколько входных параметров в своем определении в РСУБД, можно написать строго типизированный метод execute(..), который будет делегирован нетипизированному методу execute(Map) в суперклассе, как показано в следующем примере:

Java
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class TitlesAfterDateStoredProcedure extends StoredProcedure {
    private static final String SPROC_NAME = "TitlesAfterDate";
    private static final String CUTOFF_DATE_PARAM = "cutoffDate";
    public TitlesAfterDateStoredProcedure(DataSource dataSource) {
        super(dataSource, SPROC_NAME);
        declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
        declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
        compile();
    }
    public Map<String, Object> execute(Date cutoffDate) {
        Map<String, Object> inputs = new HashMap<String, Object>();
        inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
        return super.execute(inputs);
    }
}
Kotlin
import java.sql.Types
import java.util.Date
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.StoredProcedure
class TitlesAfterDateStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) {
    companion object {
        private const val SPROC_NAME = "TitlesAfterDate"
        private const val CUTOFF_DATE_PARAM = "cutoffDate"
    }
    init {
        declareParameter(SqlParameter(CUTOFF_DATE_PARAM, Types.DATE))
        declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
        compile()
    }
    fun execute(cutoffDate: Date) = super.execute(
            mapOf<String, Any>(CUTOFF_DATE_PARAM to cutoffDate))
}