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 систем.