Пакет org.springframework.jdbc.object містить класи, які дозволяють отримати доступ до бази даних більш об'єктно-орієнтованим способом. Наприклад, можна виконувати запити та отримувати результати у вигляді списку, що містить бізнес-об'єкти з даними реляційних колонок, зіставленими з властивостями бізнес-об'єкта. Також можна запускати збережені процедури і виконувати інструкції оновлення, видалення та вставки. (за винятком класу 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 = ? 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 = новий 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(SqlOd. 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. Це дуже зручний та ефективний спосіб повторного використання існуючої функціональності з об'єктом предметної області 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))
}