DAO — Data Access Object
DAO отделяет прикладной код от деталей источника данных: SQL, JDBC mapping, connection handling и database-specific errors. Контракт DAO описывает операции с persistence-моделью, а transaction boundary обычно задаёт service/use case.
Контракт
public interface UserDao {
Optional<User> findById(long id);
List<User> findActive(int limit);
long insert(User user);
boolean updateEmail(long id, String email, long expectedVersion);
}Явный Optional показывает отсутствие строки. updateEmail возвращает факт изменения; expectedVersion позволяет optimistic locking.
JDBC-реализация
public final class JdbcUserDao implements UserDao {
private final DataSource dataSource;
public JdbcUserDao(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Optional<User> findById(long id) {
String sql = """
select id, username, email, version
from app_user
where id = ?
""";
try (Connection connection = dataSource.getConnection();
PreparedStatement statement =
connection.prepareStatement(sql)) {
statement.setLong(1, id);
try (ResultSet result = statement.executeQuery()) {
if (!result.next()) {
return Optional.empty();
}
return Optional.of(mapUser(result));
}
} catch (SQLException ex) {
throw new DataAccessException("Cannot load user", ex);
}
}
}DataSource предоставляет pooled connections в production. try-with-resources возвращает connection pool после закрытия; это не обязательно физическое закрытие socket.
DAO, Repository и ORM
Термины пересекаются. DAO часто ближе к таблицам и операциям storage. Repository стремится выглядеть как коллекция aggregate roots и выражать domain language. В небольшом приложении один interface может выполнять обе роли; важнее ясная граница и отсутствие утечки persistence details.
Transaction boundary
Перевод денег требует двух updates в одной transaction. Если каждый DAO method сам открывает и commit-ит connection, атомарность потеряна. Service получает transaction context или работает под @Transactional, а несколько DAO используют одну transaction.
Производительность
DAO не должен скрывать сетевой/SQL cost настолько, чтобы цикл выглядел бесплатным:
for (long id : ids) {
dao.findById(id); // N запросов
}Для набора нужен batch method, IN, join или projection. См. Кейс N plus 1 при обходе иерархии.
Типичные сбои
- Возвращать
nullбез договорённой семантики. - Catch
SQLExceptionи потерять cause/SQLState. - Открыть новую transaction в каждом DAO method составной операции.
- Возвращать наружу живой
ResultSet. - Строить динамический SQL из пользовательского значения.
- Загружать entity по одной в цикле.
Вопросы для собеседования
Что скрывает DAO? Детали протокола доступа, SQL/mapping и resource handling.
Где лучше держать transaction boundary? На уровне business use case, объединяющего нужные DAO operations.
Зачем DataSource, а не один общий Connection? Он управляет получением connections и обычно интегрируется с pool/transactions; один Connection не thread-safe ресурс приложения.
Чем DAO отличается от Repository? DAO чаще storage-oriented, Repository — domain aggregate-oriented; граница зависит от архитектуры.
Почему batch method важен? Делает стоимость набора явной и избегает N round trips.
См. Структура PreparedStatement, Паттерны, Анализ производительности ORM систем.