Привет, Хабр! Сегодня мы рассмотрим интересный паттерн Data Mapper и его реализацию на Java. Data Mapper аккуратно переносит данные между объектами и базой данных, не вмешиваясь в логику самих объектов. Основная идея заключается в том, чтобы создать отдельный слой или компонент, который отвечает за перенос данных между объектами доменной модели и базой данных, при этом объекты домена остаются полностью независимыми от механизмов хранения данных.
В отличие от Active Record, где объекты сами знают, как сохранять себя в базу, Data Mapper разделяет эти ответственности.
Реализация Data Mapper на Java
Начнём с создания класса Cat
, который будет представлять объект домена.
public class Cat {
private int id;
private String name;
private String breed;
private int age;
// Конструкторы, геттеры и сеттеры
public Cat() {}
public Cat(int id, String name, String breed, int age) {
this.id = id;
this.name = name;
this.breed = breed;
this.age = age;
}
// Геттеры и сеттеры...
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
// Аналогично для name, breed и age
}
Теперь есть основа для взаимодействия с базой данных.
Определим интерфейс CatMapper
, который будет содержать методы для работы с базой данных.
public interface CatMapper {
void insert(Cat cat) throws SQLException;
Cat findById(int id) throws SQLException;
List<Cat> findAll() throws SQLException;
void update(Cat cat) throws SQLException;
void delete(int id) throws SQLException;
}
Этот интерфейс задаёт основные CRUD-операции для управления объектами Cat
в базе данных. Включение SQLException
указывает на возможные ошибки при взаимодействии с базой.
Теперь реализуем CatMapper
с JDBC. Будем использовать PreparedStatement
для предотвращения SQL-инъекций.
public class CatMapperImpl implements CatMapper {
private final Connection connection;
public CatMapperImpl(Connection connection) {
this.connection = connection;
}
@Override
public void insert(Cat cat) throws SQLException {
String sql = "INSERT INTO cats (name, breed, age) VALUES (?, ?, ?)";
try (PreparedStatement stmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
stmt.setString(1, cat.getName());
stmt.setString(2, cat.getBreed());
stmt.setInt(3, cat.getAge());
stmt.executeUpdate();
try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
cat.setId(generatedKeys.getInt(1));
}
}
}
}
@Override
public Cat findById(int id) throws SQLException {
String sql = "SELECT * FROM cats WHERE id = ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setInt(1, id);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return mapRow(rs);
}
}
}
return null;
}
@Override
public List<Cat> findAll() throws SQLException {
List<Cat> cats = new ArrayList<>();
String sql = "SELECT * FROM cats";
try (Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
cats.add(mapRow(rs));
}
}
return cats;
}
@Override
public void update(Cat cat) throws SQLException {
String sql = "UPDATE cats SET name = ?, breed = ?, age = ? WHERE id = ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, cat.getName());
stmt.setString(2, cat.getBreed());
stmt.setInt(3, cat.getAge());
stmt.setInt(4, cat.getId());
stmt.executeUpdate();
}
}
@Override
public void delete(int id) throws SQLException {
String sql = "DELETE FROM cats WHERE id = ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setInt(1, id);
stmt.executeUpdate();
}
}
private Cat mapRow(ResultSet rs) throws SQLException {
return new Cat(
rs.getInt("id"),
rs.getString("name"),
rs.getString("breed"),
rs.getInt("age")
);
}
}
Здесь реализовали все методы интерфейса CatMapper
с использованием безопасных PreparedStatement
. Метод mapRow
помогает преобразовать результат запроса в объект Cat
.
Теперь посмотрим, как можно использовать CatMapper в приложении.
public class CatService {
private final CatMapper catMapper;
public CatService(CatMapper catMapper) {
this.catMapper = catMapper;
}
public void addNewCat(String name, String breed, int age) {
Cat cat = new Cat();
cat.setName(name);
cat.setBreed(breed);
cat.setAge(age);
try {
catMapper.insert(cat);
System.out.println("Котик добавлен с ID: " + cat.getId());
} catch (SQLException e) {
System.err.println("Не удалось добавить котика: " + e.getMessage());
}
}
public void printAllCats() {
try {
List<Cat> cats = catMapper.findAll();
cats.forEach(cat -> System.out.println(cat.getName() + " - " + cat.getBreed()));
} catch (SQLException e) {
System.err.println("Не удалось получить список котиков: " + e.getMessage());
}
}
public void updateCatAge(int id, int newAge) {
try {
Cat cat = catMapper.findById(id);
if (cat != null) {
cat.setAge(newAge);
catMapper.update(cat);
System.out.println("Возраст котика " + cat.getName() + " обновлён до " + newAge);
} else {
System.out.println("Котик с ID " + id + " не найден.");
}
} catch (SQLException e) {
System.err.println("Не удалось обновить возраст котика: " + e.getMessage());
}
}
public void deleteCat(int id) {
try {
catMapper.delete(id);
System.out.println("Котик с ID " + id + " удалён.");
} catch (SQLException e) {
System.err.println("Не удалось удалить котика: " + e.getMessage());
}
}
}
В CatService
инкапсулируем бизнес-логику, взаимодействуя с CatMapper
. Методы добавляют, выводят, обновляют и удаляют котиков, обрабатывая возможные исключения.
Не забудем настроить подключение к базе данных. Здесь я использую HikariCP для управления пулом соединений — потому что, как и котики, соединения тоже любят комфорт.
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
public class Database {
private static HikariDataSource dataSource;
static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/catdb?useSSL=false&serverTimezone=UTC");
config.setUsername("root");
config.setPassword("password");
config.setMaximumPoolSize(10);
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
dataSource = new HikariDataSource(config);
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
Этот класс настраивает пул соединений с базой данных MySQL, используя HikariCP. Параметры конфигурации оптимизируют производительность и безопасность соединений.
И, наконец, соберем все вместе и запустим приложение.
public class Main {
public static void main(String[] args) {
try (Connection connection = Database.getConnection()) {
CatMapper catMapper = new CatMapperImpl(connection);
CatService catService = new CatService(catMapper);
// Добавляем новых котиков
catService.addNewCat("Мурзик", "Сиамская", 3);
catService.addNewCat("Барсик", "Персидская", 5);
// Выводим всех котиков
catService.printAllCats();
// Обновляем возраст котика
catService.updateCatAge(1, 4);
// Удаляем котика
catService.deleteCat(2);
// Выводим всех котиков после изменений
catService.printAllCats();
} catch (SQLException e) {
System.err.println("Ошибка подключения к базе данных: " + e.getMessage());
}
}
}
В Main
инициализируем соединение, создаем CatMapper
и CatService
, добавляем котиков, выводим их, обновляем возраст и удаляем.
Запустив приложение, можно увидеть следующее:
Котик добавлен с ID: 1
Котик добавлен с ID: 2
Мурзик - Сиамская
Барсик - Персидская
Возраст котика Мурзик обновлён до 4
Котик с ID 2 удалён.
Мурзик - Сиамская
Нашими пушистыми друзьями сначала стали Мурзик и Барсик, потом мы обновили возраст Мурзика и удалили Барсика, оставив только одного котика на экране.
Прочие практики
В жизни котики иногда требуют особого внимания. Точно так же и транзакции должны быть тщательно управляемы. Используйте транзакции, чтобы гарантировать целостность данных.
public void performTransactionalOperation() {
try {
connection.setAutoCommit(false);
// Выполняем несколько операций
catMapper.insert(new Cat("Киса", "Британская", 2));
Cat existingCat = catMapper.findById(1);
if (existingCat != null) {
existingCat.setAge(existingCat.getAge() + 1);
catMapper.update(existingCat);
}
connection.commit();
System.out.println("Транзакция успешно завершена.");
} catch (SQLException e) {
try {
connection.rollback();
System.err.println("Транзакция откатилась из-за ошибки: " + e.getMessage());
} catch (SQLException rollbackEx) {
System.err.println("Ошибка при откате транзакции: " + rollbackEx.getMessage());
}
} finally {
try {
connection.setAutoCommit(true);
} catch (SQLException e) {
System.err.println("Не удалось вернуть авто-коммит в состояние true: " + e.getMessage());
}
}
}
Отключаем авто-коммит, выполняем несколько операций, и, если все прошло успешно — подтверждаем транзакцию. В случае ошибки откатываем изменения, чтобы сохранить целостность данных.
А чтобы избежать постоянных запросов к базе, можно внедрить кэширование.
public class CachingCatMapper implements CatMapper {
private final CatMapper delegate;
private final Map<Integer, Cat> cache = new HashMap<>();
public CachingCatMapper(CatMapper delegate) {
this.delegate = delegate;
}
@Override
public void insert(Cat cat) throws SQLException {
delegate.insert(cat);
cache.put(cat.getId(), cat);
}
@Override
public Cat findById(int id) throws SQLException {
return cache.computeIfAbsent(id, delegate::findById);
}
@Override
public List<Cat> findAll() throws SQLException {
if (cache.isEmpty()) {
List<Cat> cats = delegate.findAll();
cats.forEach(cat -> cache.put(cat.getId(), cat));
}
return new ArrayList<>(cache.values());
}
@Override
public void update(Cat cat) throws SQLException {
delegate.update(cat);
cache.put(cat.getId(), cat);
}
@Override
public void delete(int id) throws SQLException {
delegate.delete(id);
cache.remove(id);
}
}
Этот класс оборачивает оригинальный CatMapper
, добавляя слой кэширования. Теперь частые запросы к одним и тем же данным будут обрабатываться быстрее, без обращения к базе.
Не забывайте писать тесты! Data Mapper упрощает тестирование, т.к бизнес-логика отделена от доступа к данным. Можно использовать Mockito для создания моков CatMapper
.
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
public class CatServiceTest {
@Test
public void testAddNewCat() throws SQLException {
// Создаём мок CatMapper
CatMapper mockMapper = mock(CatMapper.class);
CatService catService = new CatService(mockMapper);
// Вызываем метод добавления котика
catService.addNewCat("Васька", "Абиссинская", 4);
// Захватываем аргумент, переданный в метод insert
ArgumentCaptor<Cat> captor = ArgumentCaptor.forClass(Cat.class);
verify(mockMapper).insert(captor.capture());
// Проверяем, что котик был правильно создан
Cat capturedCat = captor.getValue();
assertEquals("Васька", capturedCat.getName());
assertEquals("Абиссинская", capturedCat.getBreed());
assertEquals(4, capturedCat.getAge());
}
// Другие тесты...
}
Используем Mockito для создания мок-объекта CatMapper
, проверяем, что метод insert
вызывается с правильными параметрами. Это позволяет протестировать CatService
независимо от реальной базы данных.
Когда Data mapper не решение?
Data Mapper не подходит для небольших проектов, где его сложность избыточна, или для MVP, где важнее скорость разработки. Его реализация требует значительных затрат времени на маппинг, тесты и поддержку кода, что может быть неоправданным для простых CRUD-операций. В таких случаях, конечно же, лучше использовать Active Record или готовые ORM вроде Hibernate.
Паттерн также неудобен при работе с частыми изменениями схемы БД или сложными графами данных. Абстракция Data Mapper может снизить производительность в высоконагруженных системах, где прямой доступ к базе предпочтительнее. Для проектов с простыми требованиями или высокой динамикой стоит выбирать, к примеру, Repository или Active Record.
Больше актуальных навыков по архитектуре приложений вы можете получить в рамках практических онлайн-курсов от экспертов отрасли.
Сегодня, 27 декабря, в 20:00 пройдет открытый урок, посвященный теме контейнеризации Java-приложений с Docker. Если интересно, записывайтесь по ссылке.