Skip to content

Работа с базой данных через Spring

Синопсис

В предыдущем посте Работа с базой данных через JDBC шаг за шагом создали приложение взаимодействующее с базой данных на уровне JDBC. В этом посте напишем подобное приложение, но взаимодействие с базой данных будет осуществляться через платформу Spring, в частности в этом приложении управление источником данных переложим на Spring.

Похожие посты

  • Работа с базой данных через JDBC
  • Hibernate (конфигурация в стиле xml)
  • Hibernate (конфигурация в стиле аннотации)
  • Hibernate (конфигурация spring)
  • Основы JPA
  • JPA и Spring
  • Простой Spring MVC
  • Содержание

  • Погнали!
  • База данных
  • Структура проекта
  • Java код. Модели предметной области
  • Уровень доступа к данным
  • Доступа к данным Item
  • Доступа к данным Warehouse
  • Доступа к данным Provider
  • Классы Spring, моделирующие операции JDBC
  • Хранимые функции
  • Тесты
  • Файлы конфигурации
  • Сборка и запуск
  • Погнали!

    Весь проект можно взять с gitHub:
    https://github.com/dev-blogs/database/tree/master/simple-spring-database

    База данных

    База данных в этом примере такая же как в посте Работа с базой данных через JDBC. Там приводится ее структура и как ее поднять в MySQL. Для экономии места эти детали не привожу, в этом посте к базе данных добавится только хранимая функция.

    Структура проекта

    simple-spring-database
        ├──src
        │   ├─main
        │   │   ├─java
        │   │   │   └─com
        │   │   │       └─dev
        │   │   │           └─blogs
        │   │   │               ├─model
        │   │   │               │   ├─Item.java
        │   │   │               │   ├─Warehouse.java
        │   │   │               │   └─Provider.java
        │   │   │               ├─dao
        │   │   │               │   ├─ItemDao.java
        │   │   │               │   ├─ProviderDao.java
        │   │   │               │   ├─WarehouseDao.java
        │   │   │               │   ├─ProviderSFDao.java
        │   │   │               │   └─impl
        │   │   │               │       ├─ItemDaoImpl.java
        │   │   │               │       ├─ProviderDaoImpl.java
        │   │   │               │       ├─ProviderSFDaoImpl.java
        │   │   │               │       └─WarehouseDaoImpl.java
        │   │   │               └─jdbc
        │   │   │                   ├─DeleteProvider.java
        │   │   │                   ├─InsertItem.java
        │   │   │                   ├─InsertItemProvider.java
        │   │   │                   ├─UpdateProvider.java
        │   │   │                   ├─InsertProvider.java
        │   │   │                   ├─SelectAllProviders.java
        │   │   │                   ├─SelectProviderById.java
        │   │   │                   └─SFNameById.java
        │   │   └─resources
        │   │       ├─jdbc.properties
        │   │       ├─log4j.properties
        │   │       ├─spring
        │   │       │   ├─datasource-dbcp.xml
        │   │       │   └─sprint-context.xml
        │   │       └─database-scripts
        │   │           ├─create-data-model.sql
        │   │           ├─fill-database.sql
        │   │           └─store-function.sql
        │   └─test
        │       └─java
        │           └─com
        │               └─dev
        │                   └─blogs
        │                      ├─TestItem.java
        │                      ├─TestProvider.java
        │                      └─TestWarehouse.java
        └──pom.xml
    

    Java код

    Item.java

    package com.dev.blogs.model;
    
    import java.security.Provider;
    import java.util.HashSet;
    import java.util.Set;
    
    public class Item {
    	public static final String ALIAS_TABLE_NAME = "i";
    	public static final String TABLE_NAME = "items";
    	
    	public static final String ALIAS_ID_ITEM_COLUMN = "item_id";
    	public static final String ID_COLUMN = "id";
    	public static final String NAME_COLUMN = "name";
    	public static final String WAREHOUSE_ID_COLUMN = "warehouse_id";
    	
    	private Long id;
        private String name;
        private Long warehouse_id;
        private Set<Provider> providers = new HashSet<Provider>();
     
        public Long getId() {
            return id;
        }
     
        public void setId(Long id) {
            this.id = id;
        }
     
        public String getName() {
            return name;
        }
     
        public void setName(String name) {
            this.name = name;
        }
     
        public Set<Provider> getProviders() {
            return providers;
        }
     
        public void setProviders(Set<Provider> providers) {
            this.providers = providers;
        }
        
        public Long getWarehouseId() {
    		return warehouse_id;
    	}
    
    	public void setWarehouseId(Long warehouse_id) {
    		this.warehouse_id = warehouse_id;
    	}
    	
    	@Override
    	public boolean equals(Object obj) {
    		if (this == obj) return true;
    		if (obj == null || getClass() != obj.getClass()) return false;
    		Item that = (Item) obj;
    		if (!name.equals(that.name) || warehouse_id != that.warehouse_id) return false;
    		
    		return true;
    	}
    				
    
    	@Override
    	public String toString() {
        	return "Item[id=" + this.id + ", name=" + this.name + "]";
        }
    }
    

    Provider.java

    package com.dev.blogs.model;
    
    import java.util.HashSet;
    import java.util.Set;
    
    public class Provider {
    	public static final String TABLE_NAME = "providers";
    	
    	public static final String ID_COLUMN = "id";
    	public static final String NAME_COLUMN = "name";
    	
    	private Long id;
        private String name;
        private Set<Item> items = new HashSet<Item>();
     
        public Long getId() {
            return id;
        }
     
        public void setId(Long id) {
            this.id = id;
        }
     
        public String getName() {
            return name;
        }
     
        public void setName(String name) {
            this.name = name;
        }
     
        public Set<Item> getItems() {
            return items;
        }
     
        public void setItems(Set<Item> items) {
            this.items = items;
        }
        
        @Override
        public boolean equals(Object obj) {
        	if (this == obj) return true;
    		if (obj == null || getClass() != obj.getClass()) return false;
    		Provider that = (Provider) obj;
    		if (!name.equals(that.name)) return false;
    		return true;
        }
        
        @Override
        public String toString() {
        	return "Provider[id=" + this.id + ", name=" + this.name + "]";
        }
    }
    

    Warehouse.java

    package com.dev.blogs.model;
    
    import java.util.HashSet;
    import java.util.Set;
    
    public class Warehouse {
    	public static final String ALIAS_TABLE_NAME = "w";
    	public static final String TABLE_NAME = "warehouses";
    	public static final String ID_COLUMN = "id";
    	public static final String ADDRESS_COLUMN = "address";
    	
    	private Long id;
        private String address;
        private Set<Item> items = new HashSet<Item>();
     
        public Long getId() {
            return id;
        }
     
        public void setId(Long id) {
            this.id = id;
        }
     
        public String getAddress() {
            return address;
        }
     
        public void setAddress(String address) {
            this.address = address;
        }
     
        public Set<Item> getItems() {
            return items;
        }
     
        public void setItems(Set<Item> items) {
            this.items = items;
        }
        
        public void addItem(Item item) {
        	if (items == null) {
        		items = new HashSet<Item>();
        	}
        	items.add(item);
        }
        
        @Override
    	public boolean equals(Object obj) {
    		if (this == obj) return true;
    		if (obj == null || getClass() != obj.getClass()) return false;
    		Warehouse that = (Warehouse) obj;
    		if (!address.equals(that.address)) return false;
    		return true;
    	}
        
        @Override
        public String toString() {
        	return "Warehouse[id=" + this.id + ", address=" + this.address + "]";
        }
    }
    

    Уровень доступа к данным

    Реализуем уровень доступа к данным тремя различными способами. Первый способ для объектов Item это использование спрингового вспомогательного класса JdbcTemplate который позволяет отправлять базе данных SQL-операторы любого типа для обновления или извлечения данных.
    Второй способ для объектов Warehouse это использование похожего класса NamedParameterJdbcTemplate, который позволяет делать тоже самое, только используя параметризованные запросы, то есть если надо вставить в запрос данные, обычно это делается используя вопросики и затем с помощью класса PreparedStatement определялись их значения соблюдая последовательность, то в случае с параметризованными SQL-операторами мы просто определяем параметры в запросе основываясь на их именах, а не на последовательности.
    Тертий способ для объектов Provider это использование спринговых классов, хранящих в себе SQL-операторы.

    ItemDao.java

    package com.dev.blogs.dao;
    
    import java.util.List;
    import com.dev.blogs.model.Item;
    
    public interface ItemDao {
    	public static final String COMMA = ", ";
    	
    	public static final String SQL_FIND_ALL = "SELECT * FROM " + Item.TABLE_NAME;
    	public static final String SQL_FIND_BY_ID = SQL_FIND_ALL + " WHERE " + Item.ID_COLUMN + " = ?";
    	public static final String SQL_FIND_BY_NAME = SQL_FIND_ALL + " WHERE " + Item.NAME_COLUMN + " = ?";
    	public static final String SQL_INSERT = "INSERT INTO " + Item.TABLE_NAME + " (" + Item.NAME_COLUMN + ", " + Item.WAREHOUSE_ID_COLUMN + ") VALUES (?, ?)";
    	public static final String SQL_UPDATE = "UPDATE " + Item.TABLE_NAME + " SET " + Item.NAME_COLUMN + " = ?" + COMMA + Item.WAREHOUSE_ID_COLUMN + " = ?" + " WHERE " + Item.ID_COLUMN + " = ?";
    	public static final String SQL_DELETE = "DELETE FROM " + Item.TABLE_NAME + " WHERE " + Item.ID_COLUMN + " = ?";
    	
    	public List<Item> findAll();
    	public Item findById(Long id);
    	public Item findByName(String name);
    	public Long insert(Item item);
    	public void update(Item item);
    	public void delete(Item item);
    }
    

    WarehouseDao.java

    package com.dev.blogs.dao;
    
    import java.util.List;
    
    import com.dev.blogs.model.Item;
    import com.dev.blogs.model.Warehouse;
    
    public interface WarehouseDao {
    	public static final String ID_PARAMETER = "id";
    	public static final String ADDRESS_PARAMETER = "address";
    	public static final String COMMA = ", ";
    	
    	public static final String SQL_FIND_ALL = "SELECT * FROM " + Warehouse.TABLE_NAME;
    	public static final String SQL_FIND_WAREHOUSES_WITH_ITEMS = "SELECT "
    			+ Warehouse.ALIAS_TABLE_NAME + "." + Warehouse.ID_COLUMN + COMMA
    			+ Warehouse.ALIAS_TABLE_NAME + "." + Warehouse.ADDRESS_COLUMN + COMMA
    			+ Item.ALIAS_TABLE_NAME + "." + Item.ID_COLUMN + " AS " + Item.ALIAS_ID_ITEM_COLUMN + COMMA
    			+ Item.ALIAS_TABLE_NAME + "." + Item.NAME_COLUMN + COMMA
    			+ Item.ALIAS_TABLE_NAME + "." + Item.WAREHOUSE_ID_COLUMN
    			+ " FROM " + Warehouse.TABLE_NAME + " " + Warehouse.ALIAS_TABLE_NAME
    			+ " LEFT JOIN " + Item.TABLE_NAME + " " + Item.ALIAS_TABLE_NAME
    			+ " ON " + Warehouse.ALIAS_TABLE_NAME + "." + Warehouse.ID_COLUMN + " = " + Item.ALIAS_TABLE_NAME + "." + Item.WAREHOUSE_ID_COLUMN;
    	public static final String PARAMETERIZED_SQL_FIND_BY_ID = SQL_FIND_ALL + " where " + Warehouse.ID_COLUMN + " = :" + ID_PARAMETER;
    	public static final String PARAMETERIZED_SQL_INSERT = "INSERT INTO " + Warehouse.TABLE_NAME + " (" + Warehouse.ADDRESS_COLUMN + ") VALUES (:" + ADDRESS_PARAMETER + ")";
    	public static final String PARAMETERIZED_SQL_UPDATE = "UPDATE " + Warehouse.TABLE_NAME + " SET " + Warehouse.ADDRESS_COLUMN +" = :" + ADDRESS_PARAMETER + " WHERE " + Warehouse.ID_COLUMN + " = :" + ID_PARAMETER;
    	public static final String PARAMETERIZED_SQL_DELETE = "DELETE FROM " + Warehouse.TABLE_NAME + " WHERE " + Warehouse.ID_COLUMN + " = :" + ID_PARAMETER;	
    	
    	public List<Warehouse> findAll();
    	public List<Warehouse> findWarehousesWithItems();
    	public Warehouse findById(Long id);
    	public Long insert(Warehouse warehouse);
    	public void update(Warehouse warehouse);
    	public void delete(Warehouse warehouse);
    }
    

    ProviderDao.java

    package com.dev.blogs.dao;
    
    import java.util.List;
    import com.dev.blogs.model.Provider;
    
    public interface ProviderDao {
    	public List<Provider> findAll();
    	public Provider findById(Long id);
    	public Long insert(Provider provider);
    	public Long insertWithItems(Provider provider);
    	public void update(Provider provider);
    	public void delete(Provider provider);
    }
    

    Интерфейс ProviderSFDao предоставляет доступ к хранимой функции:

    ProviderSFDao.java

    package com.dev.blogs.dao;
    
    public interface ProviderSFDao {
    	public String getNameById(Long id);
    }
    

    Доступа к данным Item

    В этом dao-классе, для запроса данных из базы будем использовать спринговый класс JdbcTemplate. Этот класс существенно упрощает программную модель при разработке логики доступа к данных с помощью JDBC.
    В этом классе в методах запроса данных из базы, создается анонимный класс реализующий интерфейс RowMapper в котором мы реализуем метод mapRow. Этот метод трансформирует значение конкретной записи результирующего набора в необходимый объект предметной области.

    ItemDaoImpl.java

    package com.dev.blogs.dao.impl;
    
    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.util.List;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.dao.EmptyResultDataAccessException;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.core.PreparedStatementCreator;
    import org.springframework.jdbc.core.RowMapper;
    import org.springframework.jdbc.support.GeneratedKeyHolder;
    import org.springframework.jdbc.support.KeyHolder;
    import org.springframework.stereotype.Component;
    import com.dev.blogs.dao.ItemDao;
    import com.dev.blogs.model.Item;
    
    @Component
    public class ItemDaoImpl implements ItemDao {
    	protected final Log logger = LogFactory.getLog(getClass());
    	
    	@Autowired
    	private JdbcTemplate jdbcTemplate;
    
    	public List<Item> findAll() {
    		return jdbcTemplate.query(SQL_FIND_ALL, new RowMapper<Item>() {
    			public Item mapRow(ResultSet rs, int rowNum) throws SQLException {
    				Item item = new Item();
    				item.setId(rs.getLong(Item.ID_COLUMN));
    				item.setName(rs.getString(Item.NAME_COLUMN));
    				item.setWarehouseId(rs.getLong(Item.WAREHOUSE_ID_COLUMN));
    				return item;
    			}
    		});
    	}
    
    	public Item findById(Long id) {
    		Item item = null;
    		try {
    			item = jdbcTemplate.queryForObject(SQL_FIND_BY_ID, new Object[] { id }, new RowMapper<Item>() {
    				public Item mapRow(ResultSet rs, int arg) throws SQLException {
    					Item item = new Item();
    					item.setId(rs.getLong(Item.ID_COLUMN));
    					item.setName(rs.getString(Item.NAME_COLUMN));
    					item.setWarehouseId(rs.getLong(Item.WAREHOUSE_ID_COLUMN));
    					return item;
    				}
    			});
    		} catch (EmptyResultDataAccessException e) {
    			logger.info("Empty result");
    		}
    		return item;
    	}
    
    	public Item findByName(String name) {
    		Object[] objects = new Object[] { name };
    		Item item = null;
    		try {			
    			item = jdbcTemplate.queryForObject(SQL_FIND_BY_NAME, objects, new RowMapper<Item>() {
    				public Item mapRow(ResultSet rs, int arg) throws SQLException {
    					Item item = new Item();
    					item.setId(rs.getLong(Item.ID_COLUMN));
    					item.setName(rs.getString(Item.NAME_COLUMN));
    					item.setWarehouseId(rs.getLong(Item.WAREHOUSE_ID_COLUMN));
    					return item;
    				}
    			});
    		} catch (EmptyResultDataAccessException e) {
    			logger.info("Empty result");
    		}
    		return item;
    	}
    	
    	public Long insert(final Item item) {
    		PreparedStatementCreator preparedStatementCreator = new PreparedStatementCreator() {
                public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
                    PreparedStatement ps = connection.prepareStatement(SQL_INSERT, new String[] { Item.ID_COLUMN });
                    ps.setString(1, item.getName());
                    ps.setLong(2, item.getWarehouseId());
                    return ps;
                }
            };
    		KeyHolder holder = new GeneratedKeyHolder();
    		jdbcTemplate.update(preparedStatementCreator, holder);
    		return holder.getKey().longValue();
    	}
    
    	public void update(Item item) {
    		Object[] objects = new Object[] {
    			item.getName(),
    			item.getWarehouseId(),
    			item.getId()
    		};
    		jdbcTemplate.update(SQL_UPDATE, objects);
    	}
    
    	public void delete(Item item) {
    		Object[] objects = new Object[] {
    			item.getId()
    		};
    		jdbcTemplate.update(SQL_DELETE, objects);
    	}
    }
    

    В методах поиска findAll(), findById() и findByName() создается класс реализующий интерфейс RowMapper, который позволяет трансформировать результат запроса в объект предметной области.
    Так же обратим внимание на метод insert() который добавляет запись в базу. Дело в том, что в записи, которая добавляется в базу, отсутствует значение в поле id, так как первичный ключ сгенерируется только после операции вставки, когда СУРБД сгенерирует значение идентифицирующее запись. Столбец ID объявлен с атрибутом AUTO_INCREMENT, это значит, что значение ему присваивается СУРБД. Для того, чтобы извлечь сгенерированные СУРБД ключи, для MySQL (и для H2) надо создать инстанс KeyHolder и передать его в метод update, а затем, после операции вставки извлечь значение из инстанса KeyHolder.

    Доступа к данным Warehouse

    WarehouseDaoImpl.java

    package com.dev.blogs.dao.impl;
    
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.dao.DataAccessException;
    import org.springframework.dao.EmptyResultDataAccessException;
    import org.springframework.jdbc.core.ResultSetExtractor;
    import org.springframework.jdbc.core.RowMapper;
    import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
    import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
    import org.springframework.jdbc.core.namedparam.SqlParameterSource;
    import org.springframework.jdbc.support.GeneratedKeyHolder;
    import org.springframework.jdbc.support.KeyHolder;
    import org.springframework.stereotype.Component;
    import com.dev.blogs.dao.WarehouseDao;
    import com.dev.blogs.model.Item;
    import com.dev.blogs.model.Warehouse;
    
    @Component
    public class WarehouseDaoImpl implements WarehouseDao {
    	protected final Log logger = LogFactory.getLog(getClass());
    	
    	@Autowired
    	private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    
    	public List<Warehouse> findAll() {
    		return namedParameterJdbcTemplate.query(SQL_FIND_ALL, new RowMapper<Warehouse>() {
    			public Warehouse mapRow(ResultSet rs, int rowNum) throws SQLException {
    				Warehouse warehouse = new Warehouse();
    				warehouse.setId(rs.getLong(Warehouse.ID_COLUMN));
    				warehouse.setAddress(rs.getString(Warehouse.ADDRESS_COLUMN));
    				return warehouse;
    			}
    		});
    	}
    
    	public List<Warehouse> findWarehousesWithItems() {
    		return namedParameterJdbcTemplate.query(SQL_FIND_WAREHOUSES_WITH_ITEMS, new ResultSetExtractor<List<Warehouse>>() {
    			public List<Warehouse> extractData(ResultSet rs) throws SQLException, DataAccessException {
    				Map<Long, Warehouse> map = new HashMap<Long, Warehouse>();
    				Warehouse warehouse = null;
    				while (rs.next()) {
    					Long id = rs.getLong(Warehouse.ID_COLUMN);
    					warehouse = map.get(id);
    					if (warehouse == null) {
    						warehouse = new Warehouse();
    						warehouse.setId(id);
    						warehouse.setAddress(rs.getString(Warehouse.ADDRESS_COLUMN));
    						map.put(id, warehouse);
    					}
    					Long itemId = rs.getLong(Item.ALIAS_ID_ITEM_COLUMN);
    					if (itemId != null) {
    						Item item = new Item();
    						item.setId(itemId);
    						item.setName(rs.getString(Item.NAME_COLUMN));
    						item.setWarehouseId(rs.getLong(Item.WAREHOUSE_ID_COLUMN));
    						warehouse.addItem(item);
    					}
    				}
    				return new ArrayList<Warehouse>(map.values());
    			}
    		});
    	}
    
    	public Warehouse findById(Long id) {
    		SqlParameterSource sqlParameterSource = new MapSqlParameterSource(WarehouseDao.ID_PARAMETER, id);
    		Warehouse warehouse = null;
    		try {
    			warehouse = namedParameterJdbcTemplate.queryForObject(PARAMETERIZED_SQL_FIND_BY_ID, sqlParameterSource, new RowMapper<Warehouse>() {
    				public Warehouse mapRow(ResultSet rs, int arg) throws SQLException {
    					Warehouse warehouse = new Warehouse();
    					warehouse.setId(rs.getLong(Warehouse.ID_COLUMN));
    					warehouse.setAddress(rs.getString(Warehouse.ADDRESS_COLUMN));
    					return warehouse;
    				}
    			});
    		} catch (EmptyResultDataAccessException e) {
    			logger.info("Empty result");
    		}
    		return warehouse;
    	}
    
    	public Long insert(Warehouse warehouse) {
    		SqlParameterSource sqlParameterSource = new MapSqlParameterSource(WarehouseDao.ADDRESS_PARAMETER, warehouse.getAddress());
    		KeyHolder holder = new GeneratedKeyHolder();
    		namedParameterJdbcTemplate.update(PARAMETERIZED_SQL_INSERT, sqlParameterSource, holder);
    		return holder.getKey().longValue();
    	}
    
    	public void update(Warehouse warehouse) {
    		Map<String, Object> params = new HashMap<String, Object>();
    		params.put(WarehouseDao.ID_PARAMETER, warehouse.getId());
    		params.put(WarehouseDao.ADDRESS_PARAMETER, warehouse.getAddress());
    		SqlParameterSource sqlParameterSource = new MapSqlParameterSource(params);
    		namedParameterJdbcTemplate.update(PARAMETERIZED_SQL_UPDATE, sqlParameterSource);		
    	}
    
    	public void delete(Warehouse warehouse) {
    		SqlParameterSource sqlParameterSource = new MapSqlParameterSource(WarehouseDao.ID_PARAMETER, warehouse.getId());
    		namedParameterJdbcTemplate.update(PARAMETERIZED_SQL_DELETE, sqlParameterSource);
    	}
    }
    

    Рассмотрим более подробно как работает метод findWarehousesWithItems(), еще раз приведу его код:

    public List<Warehouse> findWarehousesWithItems() {
    	return namedParameterJdbcTemplate.query(SQL_FIND_WAREHOUSES_WITH_ITEMS, new ResultSetExtractor<List<Warehouse>>() {
    		public List<Warehouse> extractData(ResultSet rs) throws SQLException, DataAccessException {
    			Map<Long, Warehouse> map = new HashMap<Long, Warehouse>();
    			Warehouse warehouse = null;
    			while (rs.next()) {
    				Long id = rs.getLong(Warehouse.ID_COLUMN);
    				warehouse = map.get(id);
    				if (warehouse == null) {
    					warehouse = new Warehouse();
    					warehouse.setId(id);
    					warehouse.setAddress(rs.getString(Warehouse.ADDRESS_COLUMN));
    					map.put(id, warehouse);
    				}
    				Long itemId = rs.getLong(Item.ALIAS_ID_ITEM_COLUMN);
    				if (itemId != null) {
    					Item item = new Item();
    					item.setId(itemId);
    					item.setName(rs.getString(Item.NAME_COLUMN));
    					item.setWarehouseId(rs.getLong(Item.WAREHOUSE_ID_COLUMN));
    					warehouse.addItem(item);
    				}
    			}
    			return new ArrayList<Warehouse>(map.values());
    		}
    	});
    }
    

    Чтобы вытащить все склады с вложенными в них товарами, нужно реализовать интерфейс org.springframework.jdbc.core.ResultSetExtractor в котором переопределяется метод extractData() для трансформации результирующего набора в список объектов Warehouse, который передается в качестве параметра в метод query класса org.springframework.jdbc.core.JdbcTemplate или, в нашем случае, в org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate, вместе со строкой запроса.
    В этом методе из декартова произведения таблиц вытаскивается сначала запись warehouse а затем все записи item которые относятся к извлеченной записи warehouse. Это выглядит примерно так, допустим в базе есть три записи в таблице warehouses и пять записей в таблице items, причем к первой записи из таблицы warehouses относятся первые три записи из таблицы items, ко второй записи из таблицы warehouses относятся четвертая и пятая записи из таблицы items, а к третей записи из таблицы warehouses ничего не относится из таблицы items:

    warehouses:
    ┌────┬───────────────────────┐
    │ id │    address            │
    ├────┼───────────────────────┤
    │  1 │ ul. one, 5            │
    │  2 │ ul. two, 4            │
    │  3 │ ul. three, 7          │
    └────┴───────────────────────┘
    items:
    ┌────┬────────────────┬──────────────┐
    │ id │ name           │ warehouse_id │
    ├────┼────────────────┼──────────────┤
    │  1 │ red table      │       1      │
    │  2 │ blue table     │       1      │
    │  3 │ black chair    │       1      │
    │  4 │ red chair      │       2      │
    │  5 │ green chair    │       2      │
    └────┴────────────────┴──────────────┘
    

    В результате запроса:

    SELECT w.id, w.address, i.id AS item_id, i.name, i.warehouse_id
    FROM warehouses w
    LEFT JOIN items i
    ON w.id = i.warehouse_id
    

    Декартово произведение таблиц, будет выглядеть так:

    ┌────┬───────────────────────┬─────────┬─────────────┬──────────────┐
    │ id │    address            │ item_id │    name     │ warehouse_id │
    ├────┼───────────────────────┼─────────┼─────────────┼──────────────┤
    │  1 │ ul. one, 5            │    1    │ red table   │      1       │
    │  1 │ ul. one, 5            │    2    │ blue table  │      1       │
    │  1 │ ul. one, 5            │    3    │ black chair │      1       │
    │  1 │ ul. one, 5            │    4    │ red chair   │      2       │
    │  1 │ ul. one, 5            │    5    │ green chair │      2       │
    │  2 │ ul. two, 4            │    1    │ red table   │      1       │
    │  2 │ ul. two, 4            │    2    │ blue table  │      1       │
    │  2 │ ul. two, 4            │    3    │ black chair │      1       │
    │  2 │ ul. two, 4            │    4    │ red chair   │      2       │
    │  2 │ ul. two, 4            │    5    │ green chair │      2       │
    │  3 │ ul. three, 7          │   NULL  │     NULL    │     NULL     │
    └────┴───────────────────────┴─────────┴─────────────┴──────────────┘
    

    Затем, из получившегося декартова произведения удаляются ненужные записи. Ненужные записи это те у которых поле id из таблицы warehouses и поле warehouse_id из таблицы items не равны:

    ┌────┬───────────────────────┬─────────┬─────────────┬──────────────┐
    │ id │    address            │ item_id │    name     │ warehouse_id │
    ├────┼───────────────────────┼─────────┼─────────────┼──────────────┤
    │  1 │ ul. one, 5            │    1    │ red table   │      1       │
    │  1 │ ul. one, 5            │    2    │ blue table  │      1       │
    │  1 │ ul. one, 5            │    3    │ black chair │      1       │
    │  1 │ ul. one, 5            │    4    │ red chair   │      2       │
    │  1 │ ul. one, 5            │    5    │ green chair │      2       │
    │  2 │ ul. two, 4            │    1    │ red table   │      1       │
    │  2 │ ul. two, 4            │    2    │ blue table  │      1       │
    │  2 │ ul. two, 4            │    3    │ black chair │      1       │
    │  2 │ ul. two, 4            │    4    │ red chair   │      2       │
    │  2 │ ul. two, 4            │    5    │ green chair │      2       │
    │  3 │ ul. three, 7          │   NULL  │     NULL    │     NULL     │
    └────┴───────────────────────┴─────────┴─────────────┴──────────────┘
    

    Окончательная таблица примет вид:

    ┌────┬───────────────────────┬─────────┬─────────────┬──────────────┐
    │ id │    address            │ item_id │    name     │ warehouse_id │
    ├────┼───────────────────────┼─────────┼─────────────┼──────────────┤
    │  1 │ ul. one, 5            │    1    │ red table   │      1       │
    │  1 │ ul. one, 5            │    2    │ blue table  │      1       │
    │  1 │ ul. one, 5            │    3    │ black chair │      1       │
    │  2 │ ul. two, 4            │    4    │ red chair   │      2       │
    │  2 │ ul. two, 4            │    5    │ green chair │      2       │
    │  3 │ ul. three, 7          │   NULL  │     NULL    │     NULL     │
    └────┴───────────────────────┴─────────┴─────────────┴──────────────┘
    

    Заметим, что одна запись из таблицы warehouses (запись с айдишником 3) осталась нетронутой, хотя она не имеет соответствующих записей из таблицы items. Это потому что мы сделали левое объединение таблиц, то есть из левой таблицы берутся все записи, а оставляются только те, у которых совпадают айдишники либо те, у которых соответствующих записей из пристыковываемой таблицы нет.
    Теперь рассмотрим, что происходит в методе extractMap() класса org.springframework.jdbc.core.ResultSetExtractor.
    Результат выборки таблицы будет храниться в карте, где ключем будет айдишник из базы, а значением объект Warehouse.
    Теперь пройдем шаг за шагом как происходит формирование карты.

    1. Сначала из результирующего набора берется первая запись, и вытаскивается из нее поле id, затем по этому айдишнику в карте ищется объект Warehouse. Так как его там нету, то создается новый объект Warehouse в котором просечиваются все поля из из первого из второго столбцов таблицы, то есть тех, которые относятся в складу и созданный объект Warehouse кладется в карту. После этого (но еще в той же самой итерации) из таблицы вытаскивается поле item_id, и если оно ноль или не NULL, то формируется объект Item оставшимися столбцами, который затем засовывается в созданный или вытащенный из карты объект Warehouse.
    2. На второй итерации, из результирующего набора опять вытаскивается айдишник, и по нему ищется объект Warehouse в карте, который, на этот раз, уже там есть. Ну а дальше формируется следующий объект Item который кладется в карту и так до тех пор пока не будет вытащенный новый айдшиник, для которого объекта Warehouse в карте еще нет. Затем создастся новый объект Warehouse который поместится в карту и дальше все повторяется.

    Доступа к данным Provider

    В классе доступа к данным ProviderDaoImpl реализуем логику доступа к данным в более объектно-ориентированной манере. То есть для каждой операции в базе данных, сохранение, поиск, обновление и пр. будем использовать целевой класс. Для этого вынесем всю логику запроса, трансформацию результирующего набора в объекты предметной области, из dao-класса в отдельный класс, который будет использоваться dao-классом, а тот отдельный класс, в свою очередь, будет наследоваться от спрингового класса, который реализует операции над данными JDBC.

    ProviderDaoImpl.java

    package com.dev.blogs.dao.impl;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.support.GeneratedKeyHolder;
    import org.springframework.jdbc.support.KeyHolder;
    import org.springframework.stereotype.Component;
    import com.dev.blogs.dao.ProviderDao;
    import com.dev.blogs.jdbc.DeleteProvider;
    import com.dev.blogs.jdbc.InsertItem;
    import com.dev.blogs.jdbc.InsertItemProvider;
    import com.dev.blogs.jdbc.InsertProvider;
    import com.dev.blogs.jdbc.SelectAllProviders;
    import com.dev.blogs.jdbc.SelectProviderById;
    import com.dev.blogs.jdbc.UpdateProvider;
    import com.dev.blogs.model.Item;
    import com.dev.blogs.model.Provider;
    
    @Component
    public class ProviderDaoImpl implements ProviderDao {
    	protected final Log logger = LogFactory.getLog(getClass());
    	
    	@Autowired
    	private SelectAllProviders selectAllProviders;
    	@Autowired
    	private SelectProviderById selectProviderById;
    	@Autowired
    	private InsertProvider insertProvider;
    	@Autowired
    	private InsertItem insertItem;
    	@Autowired
    	private InsertItemProvider insertItemProvider;
    	@Autowired
    	private UpdateProvider updateProvider;
    	@Autowired
    	private DeleteProvider deleteProvider;
    	
    	public List<Provider> findAll() {
    		return selectAllProviders.execute();
    	}
    
    	public Provider findById(Long id) {
    		Map<String, Object> paramMap = new HashMap<String, Object>();
    		paramMap.put(SelectProviderById.ID_PARAMETER, id);
    		Provider provider = null;
    		try {
    			provider = selectProviderById.executeByNamedParam(paramMap).get(0);
    		} catch (IndexOutOfBoundsException e) {
    			logger.info("Provider with id " + id + " was not found");
    		}
    		return provider;
    	}
    
    	public Long insert(Provider provider) {
    		Map<String, Object> paramMap = new HashMap<String, Object>();
    		paramMap.put(InsertProvider.NAME_PARAMETER, provider.getName());
    		KeyHolder keyHolder = new GeneratedKeyHolder();
    		insertProvider.updateByNamedParam(paramMap, keyHolder);
    		provider.setId(keyHolder.getKey().longValue());
    		logger.info("New provider inserted with id: " + provider.getId());
    		return provider.getId();
    	}
    
    	public Long insertWithItems(Provider provider) {
    		Map<String, Object> paramMap = new HashMap<String, Object>();
    		paramMap.put(InsertProvider.NAME_PARAMETER, provider.getName());
    		KeyHolder keyHolder = new GeneratedKeyHolder();
    		insertProvider.updateByNamedParam(paramMap, keyHolder);
    		provider.setId(keyHolder.getKey().longValue());
    		logger.info("New provider inserted with id: " + provider.getId());
    		
    		// Пакетна вставка для items
    		Set<Item> items = provider.getItems();
    		if (items != null) {
    			for (Item item : items) {
    				paramMap = new HashMap<String, Object>();
    				paramMap.put(InsertItem.NAME_PARAMETER, item.getName());
    				keyHolder = new GeneratedKeyHolder();
    				insertItem.updateByNamedParam(paramMap, keyHolder);
    				item.setId(keyHolder.getKey().longValue());
    
    				paramMap = new HashMap<String, Object>();
    				paramMap.put(InsertItemProvider.ITEM_ID_PARAMETER, item.getId());
    				paramMap.put(InsertItemProvider.PROVIDER_ID_PARAMETER, provider.getId());
    				insertItemProvider.updateByNamedParam(paramMap);
    			}
    		}
    		insertItem.flush();
    		insertItemProvider.flush();
    		return provider.getId();
    	}
    
    	public void update(Provider provider) {
    		Map<String, Object> paramMap = new HashMap<String, Object>();
    		paramMap.put(UpdateProvider.ID_PARAMETER, provider.getId());
    		paramMap.put(UpdateProvider.NAME_PARAMETER, provider.getName());
    		updateProvider.updateByNamedParam(paramMap);
    		logger.info("Existing provider updated with id: " + provider.getId());
    	}
    
    	public void delete(Provider provider) {
    		Map<String, Object> paramMap = new HashMap<String, Object>();
    		paramMap.put(DeleteProvider.ID_PARAMETER, provider.getId());
    		deleteProvider.updateByNamedParam(paramMap);
    		logger.info("Provider with id " + provider.getId() + " will be removed");
    	}
    }
    

    ProviderSFDaoImpl.java

    package com.dev.blogs.dao.impl;
    
    import java.util.List;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import com.dev.blogs.dao.ProviderSFDao;
    import com.dev.blogs.jdbc.SFNameById;
    
    @Component
    public class ProviderSFDaoImpl implements ProviderSFDao {
    	@Autowired
    	private SFNameById sfNameById;
    
    	public String getNameById(Long id) {
    		List<String> result = sfNameById.execute(id);
    		try {
    			return result.get(0);
    		} catch (IndexOutOfBoundsException e) {
    			return null;
    		}
    	}
    }
    

    Классы Spring, моделирующие операции JDBC

    Класс наследуемый от класса SqlUpdate служит для обновления данных в базе. Этот класс хранит внутри себя SQL-оператор обновления, позволяет привязывать к запросу SQL-параметры, извлекать сгенерированный СУРБД ключ после вставки новой записи.

    InsertProvider.java

    package com.dev.blogs.jdbc;
    
    import java.sql.Types;
    import javax.annotation.PostConstruct;
    import javax.sql.DataSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.SqlParameter;
    import org.springframework.jdbc.object.SqlUpdate;
    import org.springframework.stereotype.Component;
    import com.dev.blogs.model.Provider;
    
    @Component
    public class InsertProvider extends SqlUpdate {
    	public static final String NAME_PARAMETER = "name";
    	
    	private static final String PARAMETERIZED_SQL_INSERT_PROVDER = "INSERT INTO "
    			+ Provider.TABLE_NAME + " (" + Provider.NAME_COLUMN
    			+ ") VALUES (:" + NAME_PARAMETER + ")";	
    	
    	@Autowired
    	private DataSource dataSource;
    	
    	@PostConstruct
    	public void init() {
    		setDataSource(dataSource);
    		setSql(PARAMETERIZED_SQL_INSERT_PROVDER);
    		declareParameter(new SqlParameter(NAME_PARAMETER, Types.VARCHAR));
    		// Объявляем имя столбца для которого СУРБД генерирует ключ
    		setGeneratedKeysColumnNames(new String[] { Provider.ID_COLUMN });
    		// Заставить лежащий в основе драйвер JDBC извлечь сгенерированный ключ
    		setReturnGeneratedKeys(true);
    	}
    }
    

    Следующие два класса используют пакетные операции обновления наследуя спринговый класс BatchSqlUpdate. Работа с классом BatchSqlUpdate подобна SqlUpdate только добавляться записи в базу данных будут не по одной, а пачками. Это значит, что перед вставкой нескольких записей, мы их сначала накапливаем в массиве или в списке, а затем этот список передаем инстансу класса, наследуемому от класса BatchSqlUpdate который произведет пакетную вставку. Дальше в тестах, мы применим этот класс для добавления в базу поставщика со всеми деталями, которые он поставляет. Вот для добавления деталей понадобится пакетная вставка.

    InsertItem.java

    package com.dev.blogs.jdbc;
    
    import java.sql.Types;
    import javax.annotation.PostConstruct;
    import javax.sql.DataSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.SqlParameter;
    import org.springframework.jdbc.object.BatchSqlUpdate;
    import org.springframework.stereotype.Component;
    import com.dev.blogs.model.Item;
    
    @Component
    public class InsertItem extends BatchSqlUpdate {
    	public static final String NAME_PARAMETER = "name";
    	private static final String SQL_INSERT_ITEMS = "INSERT INTO " + Item.TABLE_NAME
    			+ " (" + Item.NAME_COLUMN + ")"
    			+ " VALUES (:" + NAME_PARAMETER + ")";
    	private static final int BATCH_SIZE = 10;
    	
    	@Autowired
    	private DataSource dataSource;
    	
    	@PostConstruct
    	public void init() {
    		setDataSource(dataSource);
    		setSql(SQL_INSERT_ITEMS);
    		declareParameter(new SqlParameter(NAME_PARAMETER, Types.VARCHAR));
    		setBatchSize(BATCH_SIZE);
    		// Объявляем имя столбца для которого СУРБД генерирует ключ
    		setGeneratedKeysColumnNames(new String[] { Item.ID_COLUMN });
    		// Заставить лежащий в основе драйвер JDBC извлечь сгенерированный ключ
    		setReturnGeneratedKeys(true);
    	}
    }
    

    InsertItemProvider

    package com.dev.blogs.jdbc;
    
    import java.sql.Types;
    import javax.annotation.PostConstruct;
    import javax.sql.DataSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.SqlParameter;
    import org.springframework.jdbc.object.BatchSqlUpdate;
    import org.springframework.stereotype.Component;
    
    @Component
    public class InsertItemProvider extends BatchSqlUpdate {
    	public static final String TABLE_NAME = "items_providers";
    	public static final String ITEM_ID_COLUMN = "item_id";
    	public static final String PROVIDER_ID_COLUMN = "provider_id";
    	
    	public static final String ITEM_ID_PARAMETER = "item_id";
    	public static final String PROVIDER_ID_PARAMETER = "provider_id";
    	
    	private static final String SQL_INSERT_ITEMS_PROVIDERS = "INSERT INTO " + TABLE_NAME
    			+ " (" + ITEM_ID_COLUMN + ", " + PROVIDER_ID_COLUMN + ")"
    			+ " VALUES (:" + ITEM_ID_PARAMETER + ", :" + PROVIDER_ID_PARAMETER + ")";
    	
    	private static final int BATCH_SIZE = 10;
    	
    	@Autowired
    	private DataSource dataSource;
    	
    	@PostConstruct
    	public void init() {
    		setDataSource(dataSource);
    		setSql(SQL_INSERT_ITEMS_PROVIDERS);
    		setBatchSize(BATCH_SIZE);
    		declareParameter(new SqlParameter(ITEM_ID_PARAMETER, Types.INTEGER));
    		declareParameter(new SqlParameter(PROVIDER_ID_PARAMETER, Types.INTEGER));
    	}
    }
    

    UpdateProvider.java

    package com.dev.blogs.jdbc;
    
    import java.sql.Types;
    import javax.annotation.PostConstruct;
    import javax.sql.DataSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.SqlParameter;
    import org.springframework.jdbc.object.SqlUpdate;
    import org.springframework.stereotype.Component;
    import com.dev.blogs.model.Provider;
    
    @Component
    public class UpdateProvider extends SqlUpdate {
    	public static final String ID_PARAMETER = "id";
    	public static final String NAME_PARAMETER = "name";
    	
    	private static final String PARAMETERIZED_SQL_UPDATE_PROVIDER = "UPDATE "
    			+ Provider.TABLE_NAME
    			+ " SET " + Provider.NAME_COLUMN + " = :" + NAME_PARAMETER
    			+ " WHERE " + Provider.ID_COLUMN + " = :" + ID_PARAMETER;
    	
    	@Autowired
    	private DataSource dataSource;
    	
    	@PostConstruct
    	public void init() {
    		setDataSource(dataSource);
    		setSql(PARAMETERIZED_SQL_UPDATE_PROVIDER);
    		declareParameter(new SqlParameter(ID_PARAMETER, Types.INTEGER));
    		declareParameter(new SqlParameter(NAME_PARAMETER, Types.VARCHAR));
    	}
    }
    

    Класс наследуемый от класса MappingSqlQuery предназначен для запроса данных. Метод mapRow() позволяет поместить результат запроса в класс-оболочку.

    SelectAllProviders.java

    package com.dev.blogs.jdbc;
    
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import javax.annotation.PostConstruct;
    import javax.sql.DataSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.object.MappingSqlQuery;
    import org.springframework.stereotype.Component;
    import com.dev.blogs.model.Provider;
    
    @Component
    public class SelectAllProviders extends MappingSqlQuery<Provider> {
    	private static final String SQL_SELECT_ALL_PROVIDERS = "SELECT * FROM " + Provider.TABLE_NAME;
    	
    	@Autowired
    	private DataSource dataSource;
    
    	@PostConstruct
    	public void init() {
    		setDataSource(dataSource);
    		setSql(SQL_SELECT_ALL_PROVIDERS);
    	}
    
    	@Override
    	protected Provider mapRow(ResultSet rs, int rowNum) throws SQLException {
    		Provider provider = new Provider();
    		provider.setId(rs.getLong(Provider.ID_COLUMN));
    		provider.setName(rs.getString(Provider.NAME_COLUMN));
    		return provider;
    	}
    }
    

    SelectProviderById.java

    package com.dev.blogs.jdbc;
    
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.sql.Types;
    import javax.annotation.PostConstruct;
    import javax.sql.DataSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.SqlParameter;
    import org.springframework.jdbc.object.MappingSqlQuery;
    import org.springframework.stereotype.Component;
    import com.dev.blogs.model.Provider;
    
    @Component
    public class SelectProviderById extends MappingSqlQuery<Provider> {
    	public static final String ID_PARAMETER = "id";
    	private static final String SQL_SELECT_ALL_PROVIDERS_BY_ID = "SELECT * FROM " + Provider.TABLE_NAME + " WHERE " + Provider.ID_COLUMN + " = :" + ID_PARAMETER;
    	
    	@Autowired
    	private DataSource dataSource;
    
    	@PostConstruct
    	public void init() {
    		setDataSource(dataSource);
    		setSql(SQL_SELECT_ALL_PROVIDERS_BY_ID);
    		declareParameter(new SqlParameter(ID_PARAMETER, Types.INTEGER));
    	}
    
    	@Override
    	protected Provider mapRow(ResultSet rs, int rowNum) throws SQLException {
    		Provider provider = new Provider();
    		provider.setId(rs.getLong(Provider.ID_COLUMN));
    		provider.setName(rs.getString(Provider.NAME_COLUMN));
    		return provider;
    	}
    }
    

    DeleteProvider.java

    package com.dev.blogs.jdbc;
    
    import java.sql.Types;
    import javax.annotation.PostConstruct;
    import javax.sql.DataSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.SqlParameter;
    import org.springframework.jdbc.object.SqlUpdate;
    import org.springframework.stereotype.Component;
    import com.dev.blogs.model.Provider;
    
    @Component
    public class DeleteProvider extends SqlUpdate {
    	public static final String ID_PARAMETER = "id";
    	
    	private static final String PARAMETERIZED_SQL_DELETE_PROVDER = "DELETE FROM "
    			+ Provider.TABLE_NAME
    			+ " WHERE " + Provider.ID_COLUMN + " = :" + ID_PARAMETER;
    	
    	@Autowired
    	private DataSource dataSource;
    	
    	@PostConstruct
    	public void init() {
    		setDataSource(dataSource);
    		setSql(PARAMETERIZED_SQL_DELETE_PROVDER);
    		declareParameter(new SqlParameter(ID_PARAMETER, Types.INTEGER));
    	}
    }
    

    Хранимые функции

    Платформа Spring предоставляет несколько классов для упрощения запуска хранимых процедур или функций. Один из таких классов SqlFunction который позволяет вызвать SQL-функции в базе данных. Для начала создадим в базе данных хранимую функцию, которую мы затем вызовем.
    Создайте sql-файл под названием store-function.sql в котором пропишем создание хранимой функции getNameById():

    store-function.sql

    USE warehouse;
    
    DELIMITER //
    CREATE FUNCTION getNameById(in_id INT)
    	RETURNS VARCHAR(60)
    BEGIN
    	RETURN (SELECT name FROM providers WHERE id = in_id);
    END //
    DELIMITER ;
    

    Затем запустим этот скрипт в базе данных MySQL:

    mysql -u root -p < store-function.sql
    

    После этого в базе данных появится хранимая функция, и ее можно вызывать из джава кода. Для этого добавим класс, расширяющий класс SqlFunction в пакете com.dev.blogs.jdbc. Этот класс представлять операцию вызова хранимой функции:

    SFNameById.java

    package com.dev.blogs.jdbc;
    
    import java.sql.Types;
    import javax.annotation.PostConstruct;
    import javax.sql.DataSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.SqlParameter;
    import org.springframework.jdbc.object.SqlFunction;
    import org.springframework.stereotype.Component;
    
    @Component
    public class SFNameById extends SqlFunction<String> {
    	private static final String CALL_STORED_FUNCTION = "SELECT getNameByid(?)";
    	
    	@Autowired
    	private DataSource dataSource;
    	
    	@PostConstruct
    	public void init() {
    		setDataSource(dataSource);
    		setSql(CALL_STORED_FUNCTION);
    		declareParameter(new SqlParameter(Types.INTEGER));
    		compile();
    	}
    }
    

    Обращаться к этому классу будет DAO-класс ProviderSFDaoImpl, который будет вызываться из тестов.

    Тесты

    TestItem.java

    package com.dev.blogs;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertNull;
    import org.junit.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
    import com.dev.blogs.dao.ItemDao;
    import com.dev.blogs.model.Item;
    
    @ContextConfiguration("classpath:spring/spring-context.xml")
    public class TestItem extends AbstractJUnit4SpringContextTests {
    	@Autowired
    	private ItemDao itemDao;
    	
    	@Test
    	public void testInsertFind() {
    		// Создать тестовый объект
    		Item item = new Item();
    		item.setName("zheka");
    		item.setWarehouseId(1l);
    		
    		// Сохранить тестовый объект в базе данных
    		Long id = itemDao.insert(item);
    		// Вытащить тестовый объект из базы данных
    		Item itemFromDb = itemDao.findById(id);
    		
    		// Сравнить вытащенный объект из базы данных с тестовым объектом
    		assertEquals(item, itemFromDb);
    		
    		// Удалить тестовый объект из базы данных
    		itemDao.delete(itemFromDb);
    	}
    	
    	@Test
    	public void testInsertFindUpdate() {
    		// Создать тестовый объект
    		Item item = new Item();
    		item.setName("zheka");
    		item.setWarehouseId(1l);
    		
    		// Сохранить тестовый объект в базе данных
    		Long id = itemDao.insert(item);
    		
    		// Вытащить тестовый объект из базы данных
    		Item itemFromDb = itemDao.findById(id);
    		
    		// Обновить в вытащенном объекте поле name
    		itemFromDb.setName("vasya");
    		// Записать обновленый тестовый объект в базу данных
    		itemDao.update(itemFromDb);
    		// После обновления повторно вытащить тестовый объект из базы данных
    		Item updatedItemFromDb = itemDao.findById(itemFromDb.getId());
    		
    		// Сравнить тестовый обновленный объект с повторно вытащенным
    		assertEquals(itemFromDb, updatedItemFromDb);
    		
    		// Удалить тестовый объект из базы данных
    		itemDao.delete(updatedItemFromDb);
    	}
    	
    	@Test
    	public void testDelete() {
    		// Создать тестовый объект
    		Item item = new Item();
    		item.setName("test message");
    		item.setWarehouseId(1l);
    		
    		// Сохранить тестовый объект в базе данных
    		Long id = itemDao.insert(item);
    		// Вытащить тестовый объект из базы данных
    		Item itemToDelete = itemDao.findById(id);
    		// Удалить тестовый объект из базы данных
    		itemDao.delete(itemToDelete);
    		// Найти удаленный объект в базе данных
    		Item itemAfterDeleting = itemDao.findById(id);
    		
    		// Сравнить вытащенный объект после удаления из базы данных на null
    		assertNull(itemAfterDeleting);
    	}
    }
    

    TestProvider.java

    package com.dev.blogs;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertNull;
    import java.util.HashSet;
    import java.util.Set;
    import org.junit.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
    import com.dev.blogs.dao.ItemDao;
    import com.dev.blogs.dao.ProviderDao;
    import com.dev.blogs.dao.ProviderSFDao;
    import com.dev.blogs.model.Item;
    import com.dev.blogs.model.Provider;
    
    @ContextConfiguration("classpath:spring/spring-context.xml")
    public class TestProvider extends AbstractJUnit4SpringContextTests {
    	@Autowired
    	private ProviderDao providerDao;
    	@Autowired
    	private ItemDao itemDao;
    	@Autowired
    	private ProviderSFDao providerSFDao;
    	
    	/**
    	 * Тест хранимой функции
    	 */
    	@Test
    	public void testStoredFunction() {
    		String name = providerSFDao.getNameById(1l);
    		assertEquals(name, "Vasya");
    	}
    	
    	@Test
    	public void testInsertProviderWithItems() {
    		// Создать тестовый объект Provider
    		Provider provider = new Provider();
    		provider.setName("test provider");
    		
    		// Создать тестовый объекты Item
    		Set<Item> items = new HashSet<Item>();
    		items.add(new Item("item1"));
    		items.add(new Item("item2"));
    		items.add(new Item("item3"));
    		items.add(new Item("item4"));
    		items.add(new Item("item5"));
    		
    		provider.setItems(items);
    		
    		// Сохранить тестовый объект Provider в базе данных
    		Long id = providerDao.insertWithItems(provider);
    		// Вытащить тестовый объект из базы данных
    		Provider providerFromDb = providerDao.findById(id);
    		
    		// Сравнить вытащенный объект из базы данных с тестовым объектом
    		assertEquals(provider, providerFromDb);
    		
    		// Вытащить из вытащенного объекта Provider набор объектов Item
    		Set<Item> itemsFromDb = provider.getItems();
    		
    		// Пройти по каждому объекту Item и сравнить с исходными
    		for (Item itemFromDb : itemsFromDb) {
    			// Проверить входит ли вытащенный объект item из базы данных в набор items тестового объекта Provider
    			assertEquals(items.contains(itemFromDb), true);
    			// Удалить тестовый объект Item из базы данных
    			itemDao.delete(itemFromDb);
    		}
    		
    		// Удалить тестовый объект Provider из базы данных
    		providerDao.delete(providerFromDb);
    	}		
    	
    	@Test
    	public void testInsertFind() {		
    		// Создать тестовый объект
    		Provider provider = new Provider();
    		provider.setName("test provider");
    
    		// Сохранить тестовый объект в базе данных
    		Long id = providerDao.insert(provider);
    		// Вытащить тестовый объект из базы данных
    		Provider providerFromDb = providerDao.findById(id);
    		
    		// Сравнить вытащенный объект из базы данных с тестовым объектом
    		assertEquals(provider, providerFromDb);
    		
    		// Удалить тестовый объект из базы данных
    		providerDao.delete(providerFromDb);		
    	}
    	
    	@Test
    	public void testInsertFindUpdate() {
    		// Создать тестовый объект
    		Provider provider = new Provider();
    		provider.setName("test provider");
    		
    		// Сохранить тестовый объект в базе данных
    		Long id = providerDao.insert(provider);
    		
    		// Вытащить тестовый объект из базы данных
    		Provider providerFromDb = providerDao.findById(id);
    		
    		// Обновить в вытащенном объекте поле name
    		providerFromDb.setName("other provider");
    		// Записать обновленый тестовый объект в базу данных
    		providerDao.update(providerFromDb);
    		
    		// После обновления повторно вытащить тестовый объект из базы данных
    		Provider updatedProviderFromDb = providerDao.findById(providerFromDb.getId());
    		
    		// Сравнить тестовый обновленный объект с повторно вытащенным
    		assertEquals(providerFromDb, updatedProviderFromDb);
    		
    		// Удалить тестовый объект из базы данных
    		providerDao.delete(updatedProviderFromDb);
    	}
    	
    	@Test
    	public void testDelete() {
    		// Создать тестовый объект
    		Provider provider = new Provider();
    		provider.setName("test provider");
    		
    		// Сохранить тестовый объект в базе данных
    		Long id = providerDao.insert(provider);
    		// Вытащить тестовый объект из базы данных
    		Provider providerFromDb = providerDao.findById(id);
    		// Удалить тестовый объект из базы данных
    		providerDao.delete(providerFromDb);
    		// Найти удаленный объект в базе данных
    		Provider providerAfterDeleting = providerDao.findById(providerFromDb.getId());
    		
    		// Сравнить вытащенный объект после удаления из базы данных на null
    		assertNull(providerAfterDeleting);
    	}
    }
    

    TestWarehouse.java

    package com.dev.blogs;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertNull;
    import java.util.List;
    import java.util.Set;
    import org.junit.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
    import com.dev.blogs.dao.WarehouseDao;
    import com.dev.blogs.model.Item;
    import com.dev.blogs.model.Warehouse;
    
    @ContextConfiguration("classpath:spring/spring-context.xml")
    public class TestWarehouse extends AbstractJUnit4SpringContextTests {
    	@Autowired
    	private WarehouseDao warehouseDao;	
    	
    	@Test
    	public void testInsertFind() {
    		// Создать тестовый объект
    		Warehouse warehouse = new Warehouse();
    		warehouse.setAddress("ul. Moskovskaya, 1");
    		
    		// Сохранить тестовый объект в базе данных
    		Long id = warehouseDao.insert(warehouse);
    		
    		// Вытащить тестовый объект из базы данных
    		Warehouse warehouseFromDb = warehouseDao.findById(id);
    		
    		// Сравнить вытащенный объект из базы данных с тестовым объектом
    		assertEquals(warehouse, warehouseFromDb);
    		
    		// Удалить тестовый объект из базы данных
    		warehouseDao.delete(warehouseFromDb);
    	}
    	
    	@Test
    	public void testInsertFindUpdateDelete() {
    		// Создать тестовый объект
    		Warehouse warehouse = new Warehouse();
    		warehouse.setAddress("ul. Moskovskaya, 1");
    		
    		// Сохранить тестовый объект в базе данных
    		Long id = warehouseDao.insert(warehouse);
    		
    		// Вытащить тестовый объект из базы данных
    		Warehouse warehouseFromDb = warehouseDao.findById(id);
    		
    		//Обновить в вытащенном объекте поле address
    		warehouseFromDb.setAddress("ul. Leningradskaya, 4");
    		// Записать обновленый тестовый объект в базу данных
    		warehouseDao.update(warehouseFromDb);
    		
    		// После обновления повторно вытащить тестовый объект из базы данных
    		Warehouse updatedWarehouseFromDb = warehouseDao.findById(warehouseFromDb.getId());
    		
    		// Сравнить тестовый обновленный объект с повторно вытащенным
    		assertEquals(warehouseFromDb, updatedWarehouseFromDb);
    		
    		// Удалить тестовый объект из базы данных
    		warehouseDao.delete(updatedWarehouseFromDb);
    	}
    	
    	@Test
    	public void testInsertFindDelete() {
    		// Создать тестовый объект
    		Warehouse warehouse = new Warehouse();
    		warehouse.setAddress("test message");
    		
    		// Сохранить тестовый объект в базе данных
    		Long id = warehouseDao.insert(warehouse);
    		// Вытащить тестовый объект из базы данных
    		Warehouse warehouseToDelete = warehouseDao.findById(id);
    		// Удалить тестовый объект из базы данных
    		warehouseDao.delete(warehouseToDelete);
    		// Найти удаленный объект в базе данных
    		Warehouse warehouseAfterDeleting = warehouseDao.findById(id);
    		
    		// Сравнить вытащенный объект после удаления из базы данных на null
    		assertNull(warehouseAfterDeleting);
    	}
    }
    

    Файлы конфигурации

    В прошлом посте для получения соединения к базе данных мы создали свой источник данных, который оборачивал объект java.sql.DriverManager, который затем возвращал объект типа java.sql.Connection. Наш источник данных имеет один существенный недостаток — каждый раз когда нужно выполнить запрос к базе данных, создается новый объект Connection, то есть создается новое соединение с базой данных. В этот раз вместо своего источника данных мы воспользуемся спринговым org.apache.commons.dbcp.BasicDataSource который управляет набором Connection и следит за их оптимальным использованием, то есть создает несколько объектов Connection, помещает их в пул, и в случае надобности достает один из объектов Connection из пула и выдает приложению, а после завершения работы с ним, объект Connection не удаляется, а помещается обратно в пул, для повторного использования.
    В файле datasource-dbcp.xml содержится та часть спрингового контекста в котором хранится конфигурация источника данных для базы данных MySQL.

    Файл jdbc.properties добавляется в src/main/resources:

    jdbc.properties

    jdbc.url=jdbc:mysql://localhost:3306/warehouse
    jdbc.login=login
    jdbc.password=password
    

    Файл datasource-dbcp.xml добавляется в src/main/resources/spring. В этот файл импортируется файл свойств jdbc.properties (строчка 15):

    datasource-dbcp.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xmlns:context="http://www.springframework.org/schema/context"
    	xsi:schemaLocation="http://www.springframework.org/schema/beans
    						http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    						http://www.springframework.org/schema/context
    						http://www.springframework.org/schema/context/spring-context-2.5.xsd">
    
    	<context:property-placeholder location="jdbc.properties" />
    	
    	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    		<property name="url">
    			<value>${jdbc.url}</value>
    		</property>
    		<property name="username">
    			<value>${jdbc.login}</value>
    		</property>
    		<property name="password">
    			<value>${jdbc.password}</value>
    		</property>
    	</bean>
    		
    </beans>
    

    Общая конфигурация Spring добавляется в src/main/resources/spring. В этот файл импортируется файл datasource-dbcp.xml (строчка 15).

    spring-context.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xmlns:context="http://www.springframework.org/schema/context"
    	xsi:schemaLocation="http://www.springframework.org/schema/beans
    						http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    						http://www.springframework.org/schema/context
    						http://www.springframework.org/schema/context/spring-context-2.5.xsd">
    
    	<import resource="datasource-dbcp.xml"/>
    	
    	<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    		<constructor-arg type="javax.sql.DataSource" ref="dataSource"/>
    	</bean>	
    	
    	<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
    		<constructor-arg type="javax.sql.DataSource" ref="dataSource"/>
    	</bean>
    
    	<context:component-scan base-package="com.dev.blogs" />
    </beans>
    

    Сборка и запуск

    pom.xml

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    
    	<groupId>com.dev.blogs</groupId>
    	<artifactId>simple-spring-database</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<packaging>jar</packaging>
    
    	<name>simple-spring-database</name>
    	<url>http://maven.apache.org</url>
    
    	<properties>
    		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    		<spring.version>3.2.7.RELEASE</spring.version>
    	</properties>
    
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-jdbc</artifactId>
    			<version>${spring.version}</version>
    		</dependency>
    		<!-- Подтягивает источник данных org.apache.commons.dbcp.BasicDataSource  -->
    		<dependency>
    			<groupId>commons-dbcp</groupId>
    			<artifactId>commons-dbcp</artifactId>
    			<version>1.2.2</version>
    		</dependency>		
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-context</artifactId>
    			<version>${spring.version}</version>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-test</artifactId>
    			<version>${spring.version}</version>
    		</dependency>
    		<dependency>
    			<groupId>mysql</groupId>
    			<artifactId>mysql-connector-java</artifactId>
    			<version>5.1.34</version>
    		</dependency>
    		<dependency>
    			<groupId>junit</groupId>
    			<artifactId>junit</artifactId>
    			<version>4.11</version>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    </project>
    

    Теперь запустим проект. Так как у нас только тесты, то тесты и будем запускать. Откройте командную строку и введите:

    mvn test
    -------------------------------------------------------
     T E S T S
    -------------------------------------------------------
    Running com.dev.blogs.TestItem
    Aug 19, 2015 3:50:46 PM org.springframework.test.context.TestContextManager retr
    ieveTestExecutionListeners
    INFO: Could not instantiate TestExecutionListener class [org.springframework.tes
    t.context.web.ServletTestExecutionListener]. Specify custom listener classes or
    make the default listener classes (and their dependencies) available.
    Aug 19, 2015 3:50:46 PM org.springframework.beans.factory.xml.XmlBeanDefinitionR
    eader loadBeanDefinitions
    INFO: Loading XML bean definitions from class path resource [spring-context.xml]
    
    Aug 19, 2015 3:50:47 PM org.springframework.context.support.AbstractApplicationC
    ontext prepareRefresh
    INFO: Refreshing org.springframework.context.support.GenericApplicationContext@1
    29a8472: startup date [Wed Aug 19 15:50:47 EEST 2015]; root of context hierarchy
    
    Aug 19, 2015 3:50:47 PM org.springframework.core.io.support.PropertiesLoaderSupp
    ort loadProperties
    INFO: Loading properties file from class path resource [jdbc.properties]
    Aug 19, 2015 3:50:47 PM org.springframework.beans.factory.support.DefaultListabl
    eBeanFactory preInstantiateSingletons
    INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.
    DefaultListableBeanFactory@75881071: defining beans [org.springframework.beans.f
    actory.config.PropertyPlaceholderConfigurer#0,dataSource,jdbcTemplate,namedParam
    eterJdbcTemplate,itemDaoImpl,providerDaoImpl,warehouseDaoImpl,deleteProvider,ins
    ertProvider,selectAllProviders,selectProviderById,updateProvider,org.springframe
    work.context.annotation.internalConfigurationAnnotationProcessor,org.springframe
    work.context.annotation.internalAutowiredAnnotationProcessor,org.springframework
    .context.annotation.internalRequiredAnnotationProcessor,org.springframework.cont
    ext.annotation.internalCommonAnnotationProcessor,org.springframework.context.ann
    otation.ConfigurationClassPostProcessor.importAwareProcessor]; root of factory h
    ierarchy
    Aug 19, 2015 3:50:48 PM com.dev.blogs.dao.impl.ItemDaoImpl findById
    INFO: Empty result
    Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.963 sec
    Running com.dev.blogs.TestProvider
    Aug 19, 2015 3:50:48 PM org.springframework.test.context.TestContextManager retr
    ieveTestExecutionListeners
    INFO: Could not instantiate TestExecutionListener class [org.springframework.tes
    t.context.web.ServletTestExecutionListener]. Specify custom listener classes or
    make the default listener classes (and their dependencies) available.
    Aug 19, 2015 3:50:48 PM com.dev.blogs.dao.impl.ProviderDaoImpl insert
    INFO: New provider inserted with id: 18
    Aug 19, 2015 3:50:48 PM com.dev.blogs.dao.impl.ProviderDaoImpl update
    INFO: Existing provider updated with id: 18
    Aug 19, 2015 3:50:48 PM com.dev.blogs.dao.impl.ProviderDaoImpl delete
    INFO: Provider with id 18 will be removed
    Aug 19, 2015 3:50:48 PM com.dev.blogs.dao.impl.ProviderDaoImpl insert
    INFO: New provider inserted with id: 19
    Aug 19, 2015 3:50:48 PM com.dev.blogs.dao.impl.ProviderDaoImpl delete
    INFO: Provider with id 19 will be removed
    Aug 19, 2015 3:50:48 PM com.dev.blogs.dao.impl.ProviderDaoImpl insert
    INFO: New provider inserted with id: 20
    Aug 19, 2015 3:50:48 PM com.dev.blogs.dao.impl.ProviderDaoImpl delete
    INFO: Provider with id 20 will be removed
    Aug 19, 2015 3:50:48 PM com.dev.blogs.dao.impl.ProviderDaoImpl findById
    INFO: Provider with id 20 was not found
    Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.267 sec
    Running com.dev.blogs.TestWarehouse
    Aug 19, 2015 3:50:48 PM org.springframework.test.context.TestContextManager retr
    ieveTestExecutionListeners
    INFO: Could not instantiate TestExecutionListener class [org.springframework.tes
    t.context.web.ServletTestExecutionListener]. Specify custom listener classes or
    make the default listener classes (and their dependencies) available.
    Aug 19, 2015 3:50:48 PM com.dev.blogs.dao.impl.WarehouseDaoImpl findById
    INFO: Empty result
    Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.213 sec
    
    Results :
    
    Tests run: 11, Failures: 0, Errors: 0, Skipped: 0
    
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 5.710 s
    [INFO] Finished at: 2015-08-19T15:50:49+03:00
    [INFO] Final Memory: 10M/155M
    [INFO] ------------------------------------------------------------------------
    

    Результатом запуска тестов должно быть Tests run: 11, Failures: 0, Errors: 0, Skipped: 0.

    Поделиться в социальных сетях

    Опубликовать в Google Plus
    Опубликовать в LiveJournal
    Опубликовать в Мой Мир
    Опубликовать в Одноклассники
    Опубликовать в Яндекс

    Добавить комментарий

    Ваш e-mail не будет опубликован. Обязательные поля помечены *