Логическое развитие идеи параметризованых запросов и prepared запросов, это инкапсуляция запросов в собственные классы. Каждый запрос обёртывается в класс, объекты которого каким-либо образом настраиваются во время исполнения и выполняют себя сами.
В этом примере используется схема данных из статьи запросы в Spring JDBC.
MappingSqlQuery
Класс MappingSqlQuery ориентирован на выполнение запросов, возвращающих данные и объединяет в себе функционал RowMapper и PreparedStatementCreator: сразу и параметры связывает сам, и результат в объект отображает сам.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | public class OrderMappingQuery extends MappingSqlQuery<Order> { private static final String ORDER_QUERY = "SELECT O.ID, CUSTOMER_ID, EMAIL " + "FROM ORDERS AS O, CUSTOMERS AS C " + "WHERE C.ID=O.CUSTOMER_ID AND O.ID=?"; public OrderMappingQuery(final JdbcTemplate jdbcTemplate) { super(jdbcTemplate.getDataSource(), ORDER_QUERY); super.declareParameter(new SqlParameter("id", Types.INTEGER)); compile(); } @Override protected final Order mapRow( final ResultSet resultSet, final int rowNum) throws SQLException { Customer customer = new Customer(); customer.setId(resultSet.getInt("customer_id")); customer.setEmail(resultSet.getString("email")); Order order = new Order(); order.setId(resultSet.getInt("id")); order.setCustomer(customer); return order; } } |
Реализация собственного MappingSqlQuery обычно состоит из двух частей. Вначале реализуют код, настраивающий MappingSqlQuery, то есть задают сам запрос, указывают какие у него параметры и т.д. Эту часть я вынес в конструктор.
Вторая часть — один в один реализация RowMapper, которая должна из JDBC ResultSet построить требуемый объект. Из-за того, что для настройки MappingSqlQuery требуется объект класса DataSource, его инициализация выполняется в методе @PostConstruct. Альтернативой этому было бы описание OrderMappingQuery как Spring bean и тогда Spring сам бы его создал, сам сконфигурировал с DataSource и сам подсунул бы в места использования.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Repository public class OrderRepositoryJdbc implements OrderRepository { @Inject private JdbcTemplate jdbcTemplate; private OrderMappingQuery orderQuery; @PostConstruct public final void init() { orderQuery = new OrderMappingQuery(jdbcTemplate); } @Override public final Order get(final Integer id) { return orderQuery.findObject(id); } } |
1 2 3 4 5 6 7 | @Test public void testGet() { Order actual = testedObject.get(100); assertThat(actual.getId(), is(100)); assertThat(actual.getCustomer().getId(), is(100)); assertThat(actual.getCustomer().getEmail(), is("TEST")); } |
SqlUpdate
Антонимом к MappingSqlQuery является SqlUpdate: всё то же самое, только не для получения данных, а для изменения данных.
1 2 3 4 5 6 7 8 9 | public class UpdateSkuDescriptionQuery extends SqlUpdate { public UpdateSkuDescriptionQuery(final JdbcTemplate jdbcTemplate) { setDataSource(jdbcTemplate.getDataSource()); setSql("UPDATE skus SET description = ? WHERE id = ?"); declareParameter(new SqlParameter("description", Types.VARCHAR)); declareParameter(new SqlParameter("id", Types.INTEGER)); compile(); } } |
В собственной реализации точно так же требуется вначале настроить объект, задав запрос, параметры соединение и прочая. Но, так как предполагается, что запрос ничего не возвращает, реализация кода отображения результатов в объект не требуется. По той же причине, из-за необходимости передавать DataSource, я и в этот раз инициализирую объект UpdateSkuDescriptionQuery в @PostConstruct методе.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | @Repository public class SkuRepositoryJdbc implements SkuRepository { private static final String DESCRIPTION_QUERY = "SELECT DESCRIPTION FROM SKUS WHERE ID = :id"; private UpdateSkuDescriptionQuery updateQuery; @Inject private NamedParameterJdbcTemplate jdbcTemplate; @Inject private JdbcTemplate simpleJdbcTemplate; @PostConstruct public final void init() { updateQuery = new UpdateSkuDescriptionQuery(simpleJdbcTemplate); } @Override public final void changeDescription(final Sku sku) { updateQuery.update(sku.getDescription(), sku.getId()); } @Override public final String getDescription(final Integer id) { return jdbcTemplate.queryForObject( DESCRIPTION_QUERY, Collections.singletonMap("id", id), String.class); } } |
1 2 3 4 5 6 7 8 9 10 | @Test public void testChange() { Sku expected = new Sku(); expected.setId(100); expected.setDescription("NEWBIE"); testedObject.changeDescription(expected); assertThat(testedObject.getDescription(100), is("NEWBIE")); } |
SimpleJdbcInsert
Поскольку из базы можно получить её метаинформацию, в частности названия и типы столбцов в таблицах, то в некоторых случаях можно даже и сам текст запроса то и не писать, только операцию и параметры. SimpleJdbcInsert это как раз тот случай: настраиваем один раз таблицу и прочие параметры и сразу можно добавлять строки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | private SimpleJdbcInsert createCustomer; @Inject private JdbcTemplate jdbcTemplate; @PostConstruct public final void init() { createCustomer = new SimpleJdbcInsert(jdbcTemplate) .withTableName("customers") .usingGeneratedKeyColumns("id"); } @Override public final void add(final Customer c) { createCustomer.execute(new BeanPropertySqlParameterSource(c)); } |
SimpleJdbcInsert принимает SqlParameterSource, что позволяет передавать в него параметры как в обычный запрос, используя Map или любую реализацию SqlParameterSource.
Код примера доступен на github.