Java. Реализация шаблона DAO

суббота, 8 февраля 2014 г.

Сегодня  я хочу наконец затронуть тему доступа к данным в Java и начать с самых основ -  с использования JDBC и шаблона DAO. Описывать DAO я не стану - в интернете полно хороших описаний этого шаблона. Я хочу заострить внимание на его реализации. Статья построена таким образом, чтобы не давать готового решения, а попытаться шаг за шагом привести Вас к нему. Поэтому не торопитесь переживать по поводу заведомо не оптимальных решений в начале, скорее всего ваши замечания будут учтены по ходу статьи.

Как я уже и сказал, в интернете множество страниц блогов и туториалов посвящено паттерну DAO, некоторые из них на столько хороши, что я просто оставлю здесь ссылки на них, и продолжу писать в полной уверенности, что они прочитаны вами:

То же самое на русском:

Пример для статьи.

Когда речь заходит о реализации, без конкретного примера трудно обойтись. В качестве такого примера я буду использовать простейшую ситуацию с двумя таблицами связанными отношением один ко многим:
Первая таблица описывает студенческие группы, их номер и название факультета; вторая таблица описывает студентов, их имя, фамилию, дату поступления и содержит внешний ключ на первую таблицу.

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

Вот пара простых sql скриптов для создания и наполнения базы данных:
create_scheme.sql
generate_content.sql

Полигон для испытаний.

Когда мы разговариваем о данных, говорить хочется именно о способах получения и объектного представления данных, и не тратить силы на их визуальное представление. Но убедиться в корректности того, что делаем все таки стоит. Для этого я буду использовать модульные тесты на JUnit.

Вернемся к DAO.

Главная задача шаблона DAO - построить мост между реляционной и объектной моделями данных. Получив представление о реляционной модели, поговорим об объектном представлении.

В случае с Group - все достаточно просто:
Но когда мы начнем проектировать объект Student,  перед нами обязательно встанет вопрос: "Как быть с ссылкой на группу?". Тут есть два варианта: 
  1. Мы можем сделать целочисленное поле, содержащее значение ключа соответствующей группы.
  2. Или объявить поле group, как содержащее ссылку на полноценный объект, описывающий группу в которой учится студент.

Первый способ. Храним значение внешнего ключа.


Такое решение, вообще говоря, не далеко уводит нас от реляционного представления данных, но может оказаться вполне приемлемым в простых проектах, в силу своей ясности и простоты реализации.

Именно с этого решения мы начнем реализацию слоя DAO.

Библиотека DAO. Начало

Реализацию шаблона DAO начнем с проектирования и реализации интерфейсов.

DaoFactory.java
GroupDao.java
StudentDao.java

Не знаю как Вам, а мне всегда не терпится увидеть первый результат своей работы. Поэтому, вместо реализации всей функциональности этап за этапом, я сосредоточусь на том, чтобы написать все необходимое для выполнения простейшей операции - вывода списка всех студенческих групп.

В качестве базы данных я использую MySQL и предлагаю простейшую реализацию слоя DAO для работы с этой СУБД:

MySqlDaoFactory.java
MySqlGroupDao.java

Реализованных на данный момент методов достаточно, чтобы отобразить список всех студенческих групп и даже немножко больше.

Напишем простейший тест, чтобы убедиться в корректности реализации:

@Test
public void testGetAll() throws Exception {
    DaoFactory daoFactory = new MySqlDaoFactory();
    List<Group> list;
    try (Connection con = daoFactory.getConnection()) {
        GroupDao dao = daoFactory.getGroupDao(con);
        list = dao.getAll();
    }
    Assert.assertNotNull(list);
    Assert.assertTrue(list.size() > 0);
}



Минутка рефакторинга

Прежде чем бросаться реализовывать остальные методы и классы, предлагаю обратить внимание на сигнатуру интерфейсов Group и Student. Фактически, они отличаются только типом возвращаемых объектов, что наводит на мысль создания единого унифицированного интерфейса:
GenericDao.java

Теперь обратим внимание на методы read(int key) и getAll() у ранее приведенной реализации MySqlGroupDao:

@Override
public Group read(int key) throws SQLException {
    String sql = "SELECT * FROM daotalk.Group WHERE id = ?;";
    Group g = new Group();
    try (PreparedStatement stm = connection.prepareStatement(sql)) {

        stm.setInt(1, key);

        ResultSet rs = stm.executeQuery();
        rs.next();
        g.setId(rs.getInt("id"));
        g.setNumber(rs.getInt("number"));
        g.setDepartment(rs.getString("department"));
    }
    return g;
}

@Override
public List<Group> getAll() throws SQLException {
    String sql = "SELECT * FROM daotalk.Group;";
    List<Group> list = new ArrayList<Group>();
    try (PreparedStatement stm = connection.prepareStatement(sql)) {
        ResultSet rs = stm.executeQuery();
        while (rs.next()) {
            Group g = new Group();
            g.setId(rs.getInt("id"));
            g.setNumber(rs.getInt("number"));
            g.setDepartment(rs.getString("department"));
            list.add(g);
        }
    }
    return list;
}

Как вы думаете, сильно ли будут отличаться одноименные методы в StudentDao? Вероятно нет.


@Override
public Student read(int key) throws SQLException {
    String sql = "SELECT * FROM daotalk.Student WHERE id = ?;";
    Student s = new Student();
    try (PreparedStatement stm = connection.prepareStatement(sql)) {

        stm.setInt(1, key);

        ResultSet rs = stm.executeQuery();
        rs.next();

        s.setId(rs.getInt("id"));
        s.setName(rs.getString("name"));
        s.setSurname(rs.getString("surname"));
        s.setEnrolmentDate(rs.getDate("enrolment_date"));
        s.setGroupId(rs.getInt("group_id"));
    }
    return s;
}

@Override
public List<Student> getAll() throws SQLException {
    String sql = "SELECT * FROM daotalk.Student;";
    List<Student> list = new ArrayList<Student>();
    try (PreparedStatement stm = connection.prepareStatement(sql)) {
        ResultSet rs = stm.executeQuery();
        while (rs.next()) {
            Student s = new Student();
            s.setId(rs.getInt("id"));
            s.setName(rs.getString("name"));
            s.setSurname(rs.getString("surname"));
            s.setEnrolmentDate(rs.getDate("enrolment_date"));
            s.setGroupId(rs.getInt("group_id"));
            list.add(s);
        }
    }
    return list;
}

Очевидно методы отличаются только синтаксисом запроса и алгоритмом разбора ResultSet. Попробуем унифицировать эти методы выделив их общие части в AbstractDao:

/**
 * Абстрактный класс предоставляющий базовую реализацию CRUD операций с использованием JDBC.
 *
 * @param <T>  тип объекта персистенции
 * @param <PK> тип первичного ключа
 */
public abstract class AbstractJDBCDao<T, PK extends Serializable> implements GenericDao<T, PK> {

    private Connection connection;

    /**
     * Возвращает sql запрос для получения всех записей.
     * <p/>
     * SELECT * FROM [Table]
     */
    public abstract String getSelectQuery();

    /**
     * Разбирает ResultSet и возвращает список объектов соответствующих содержимому ResultSet.
     */
    protected abstract List<T> parseResultSet(ResultSet rs);

    @Override
    public T getByPK(int key) throws PersistException {
        List<T> list;
        String sql = getSelectQuery();
        sql += " WHERE id = ?";
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setInt(1, key);
            ResultSet rs = statement.executeQuery();
            list = parseResultSet(rs);
        } catch (Exception e) {
            throw new PersistException(e);
        }
        if (list == null || list.size() == 0) {
            return null;
        }
        if (list.size() > 1) {
            throw new PersistException("Received more than one record.");
        }
        return list.iterator().next();
    }

    @Override
    public List<T> getAll() throws PersistException {
        List<T> list;
        String sql = getSelectQuery();
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            ResultSet rs = statement.executeQuery();
            list = parseResultSet(rs);
        } catch (Exception e) {
            throw new PersistException(e);
        }
        return list;
    }

    . . .

    public AbstractJDBCDao(Connection connection) {
        this.connection = connection;
    }
}

PersistException - это созданный класс Exception, необходимость которого я поясню позже.

Обратите внимание на то, как составляется запрос на получение записи по первичному ключу. В общем случае такая практика может оказаться не приемлемой, тогда соответствующий запрос нужно будет получать аналогично Select из специального метода.

Создание, удаление, обновление

Что на счет оставшихся методов создания, обновления и удаления записей? Аналогично методам получения данных, эти методы будут мало отличаться в реализации каждого DAO объекта. Во первых, это по прежнему будет синтаксис sql запросов, а во вторых процесс заполнения аргументов этих запросов.

@Override
public void update(T object) throws PersistException {
    String sql = getUpdateQuery();
    try (PreparedStatement statement = connection.prepareStatement(sql);) {
        prepareStatementForUpdate(statement, object); // заполнение аргументов запроса оставим на совесть потомков
        int count = statement.executeUpdate();
        if (count != 1) {
            throw new PersistException("On update modify more then 1 record: " + count);
        }
    } catch (Exception e) {
        throw new PersistException(e);
    }
}

@Override
public void delete(T object) throws PersistException {
    String sql = getDeleteQuery();
    try (PreparedStatement statement = connection.prepareStatement(sql)) {
        prepareStatementForDelete(statement, object); // заполнение аргументов запроса оставим на совесть потомков
        int count = statement.executeUpdate();
        if (count != 1) {
            throw new PersistException("On delete modify more then 1 record: " + count);
        }
        statement.close();
    } catch (Exception e) {
        throw new PersistException(e);
    }
}

Отдельного внимания заслуживает метод create и задача создания нового объекта и соответствующей ему записи. Тут есть два решения: собственно реализовать метод create, в рамках которого создавать новый объект и пустую запись, или создавать новый объект во вне, заполнять его поля и передавать его в DAO в метод persist. Реализовать заранее (в AbstractJDBCDao) первый не получится, т.к. мы не знаем его конструктора, а вот второй, persist, запросто.

@Override
public T persist(T object) throws PersistException {
    if (object.getId() != null) {
        throw new PersistException("Object is already persist.");
    }
    T persistInstance;
    // Добавляем запись
    String sql = getCreateQuery();
    try (PreparedStatement statement = connection.prepareStatement(sql)) {
        prepareStatementForInsert(statement, object);
        int count = statement.executeUpdate();
        if (count != 1) {
            throw new PersistException("On persist modify more then 1 record: " + count);
        }
    } catch (Exception e) {
        throw new PersistException(e);
    }
    // Получаем только что вставленную запись
    sql = getSelectQuery() + "WHERE id = last_insert_id();";
    try (PreparedStatement statement = connection.prepareStatement(sql)) {
        ResultSet rs = statement.executeQuery();
        List<T> list = parseResultSet(rs);
        if ((list == null) || (list.size() != 1)) {
            throw new PersistException("Exception on findByPK new persist data.");
        }
        persistInstance = list.iterator().next();
    } catch (Exception e) {
        throw new PersistException(e);
    }
    return persistInstance;
}

Обратите внимание на то, что объекты, переданный на вход метода и полученный на его выходе, совершенно разные! При желании этого можно избежать, но пока оставим как есть.

Полный код класса AbstractJDBCDao:

И так, давайте посмотрим на сколько просто стало создавать новые DAO классы. Унаследуем AbstractJDBCDao и реализуем GroupDao и StudentDao:
MySqlGroupDao.java
MySqlStudentDao.java

Не так уж и плохо! Однако метод prepareStatementForDelete постоянно дублируется в силу того, что мы не можем потребовать от наших объектов id в классе AbstractJDBCDao. Как вы и догадались, это легко решается с помощью интерфейса с единственным методом: getId(). Если все наши доменные объекты будут его реализовывать, то аргументы delete запроса можно будет устанавливать в методе delete абстрактного класса.

public abstract class AbstractJDBCDao<T extends Identified<PK>, PK extends Serializable> {

. . .

@Override
public void delete(T object) throws PersistException {
    String sql = getDeleteQuery();
    try (PreparedStatement statement = connection.prepareStatement(sql)) {
        try {
            statement.setObject(1, object.getId());
        } catch (Exception e) {
            throw new PersistException(e);
        }
        int count = statement.executeUpdate();
        if (count != 1) {
            throw new PersistException("On delete modify more then 1 record: " + count);
        }
        statement.close();
    } catch (Exception e) {
        throw new PersistException(e);
    }
}

. . .

}

Кто отвечает за идентификатор?

Если Вы обратили внимание на реализацию метода persist, то могли заметить, что задачу генерирования ключа я возлагаю на плечи СУБД. И в этом таится некоторая опасность. Дело в том, что наличие публичного метода set для идентификатора может рано или поздно привести к его противоправному изменению. Для решения этой проблемы я предлагаю следующее, возможно спорное, решение: сделать метод setId доступным только для Dao объекта. Реализовать это можно объявив setId protected методом и написав private наследника доменной сущности внутри DAO класса:

public class MySqlGroupDao extends AbstractJDBCDao<Group, Integer> implements GroupDao {

    private class PersistGroup extends Group {
        public void setId(int id) {
            super.setId(id);
        }
    }
    .  .  .
    @Override
    protected List<Group> parseResultSet(ResultSet rs) throws PersistException {
        LinkedList<Group> result = new LinkedList<Group>();
        try {
            while (rs.next()) {
                PersistGroup group = new PersistGroup(); // точка подмены реализации Group
                group.setId(rs.getInt("id"));
                group.setNumber(rs.getInt("number"));
                group.setDepartment(rs.getString("department"));
                result.add(group);
            }
        } catch (Exception e) {
            throw new PersistException(e);
        }
        return result;
    }

По сути, строка, где мы подменяем реализацию доменного объекта (в приведенном примере объекта Group) - это отправная точка его жизненного цикла, на протяжении которого значение идентификатора должно оставаться неизменным. Поэтому подменив реализацию объекта в этом месте, мы можем быть уверены, что обращение к объекту, как к экземпляру нового вложенного класса (PersistGroup), нам более нигде не понадобится.

Главным недостатком такого решения является необходимость изменения ссылки на объект после вызова метода persist:

Group g = persist(g);

С другой стороны мы можем легко определить соответствует ли объекту запись в базе данных или еще нет - достаточно проверить, определена ли ссылка на идентификатор у объекта.

Минутка рефакторинга затягивается

Мы проделали огромную работу, сократив количество кода, необходимого для реализации новых дао объектов. Осталось еще чуть-чуть - подумать, что можно сделать с фабрикой дао объектов.

Предложенный в начале статьи интерфейс фабрики предполагает написание отдельного метода для получения экземпляра дао. Это сильного ограничивает масштабируемость фабрики, вынуждая каждый раз изменять не только ее реализацию, но и интерфейс. 

Удобнее было бы иметь метод, который для указанного класса доменного объекта возвращал дао объект:

    public GenericDao getDao(Class dtoClass) throws PersistException;

Реализация такого метода достаточно проста: достаточно завести Map с ключом типа Class, возвращающую новые экземпляры соответствующий ключу дао объектов. И в этом кроется небольшая трудность - как создавать экземпляры заранее не известных классов? Можно конечно прибегнуть к рефлексии, но есть выход попроще. Объявим интерфейс делегата конструктора объектов и в нашей карте будем возвращать реализации этого интерфейса:

public interface DaoCreator<Context> {
    public GenericDao create(Context context);
}

public GenericDao getDao(Connection connection, Class dtoClass) throws PersistException {
    DaoCreator creator = creators.get(dtoClass);
    if (creator == null) {
        throw new PersistException("Dao object for " + dtoClass + " not found.");
    }
    return creator.create(connection);
}

Не понятно? Сейчас я приведу код для добавления элемента в карту и все станет гораздо понятнее!

creators = new HashMap<Class, DaoCreator>();
creators.put(Group.class, new DaoCreator<Connection>() {
    @Override
    public GenericDao create(Connection connection) {
        return new MySqlGroupDao(connection);
    }
});

Начали улавливать идею?

Прежде, чем я приведу полный код фабрики, я забегу вперед и объясню смысл PersistException и Context. Сам DAO паттерн подразумевает абстрагирование от способа хранения данных и не ограничивается использованием в этих целях реляционными базами данных. Поэтому, вообще говоря, использовать классы пакета java.sql в унифицированных классах и интерфейсах крайне не желательно. Использование собственного исключения PersistException позволяет не завязываться на SqlException. Context - сущность, описывающая сеанс взаимодействия с системой хранения данных, в случае с JDBC в качестве контекста выступает Connection.
Старый интерфейс

Новый интерфейс

MySqlDaoFactory.java

Тесты всему голова

После столь большой работы по рефакторингу кода, самое время проконтролировать его корректность. Для получающегося унифицированного решения нужен не менее унифицированный тест. Мы на самом деле можем для каждого дао объекта проконтролировать стандартные CRUD операции, за исключением операции обновления, тк в общем случае мы не знаем какие есть поля у соответствующего объекта доменной сущности.
 
GenericDaoTest.java

Пример реализации теста для MqSql фабрики:

MySqlDaoTest.java

Подытожим

Затронутая тема реализации шаблона DAO получается слишком большой, чтобы уместиться в рамках одной статьи. К настоящему моменту уже проделана достаточно большая работа: реализован унифицированные классы и интерфейсы паттерна, написана использующая их реализация  управления персистентным состоянием объектов, функционал понаписанного проверен модульными тестами и вполне жизнеспособен. Исходный код к статье можно скачать здесь: https://bitbucket.org/dok/daotalk/get/daotalk1.zip

В следующей части статьи мы наконец вернемся к поднятому в самом начале вопросу: что делать с ссылкой на группу, в которой учится студент. Рассмотрим реализацию описанных выше решений, их плюсы и минусы.

61 комментарий :

  1. Офигенная статья, жду следующей части, хотя попробую сам реализовать

    ОтветитьУдалить
  2. Хорошая статья. Но ваш код пестрит raw type. Что не есть хорошо.

    ОтветитьУдалить
    Ответы
    1. К сожалению Вы правы, но я не могу выделить время на исправление этого недостатка в обозримом будущем. Огромное Вам спасибо за то, что обратили внимание на эту проблему. (чтобы понять в чем суть замечания, стоит прочесть статью http://stackoverflow.com/questions/2770321/what-is-a-raw-type-and-why-shouldnt-we-use-it)

      Удалить
  3. подскажите, на каком слое реализовывать управление транзакциями: если требуется вызов методов из разных DAO в одной транзакции? Спасибо!

    ОтветитьУдалить
    Ответы
    1. Я бы посоветовал Вам прочесть статью о стратегиях управления транзакциями: http://www.ibm.com/developerworks/ru/library/j-ts2/.
      На сколько я понимаю, Ваш случай, это стратегия клиентского дирижирования (client orchestration transaction strategy);

      Удалить
    2. Совет пригодился. Спасибо за отклик!

      Удалить
  4. Спасибо за статью, подскажите, в чем сделаны таблички, которые Вы показываете какие свойства и методы есть у класса?

    ОтветитьУдалить
    Ответы
    1. Тоже в ней работаю, подскажите, пожалуйста, где можно найти такое схематическое представление классов. Спасибо!

      Удалить
    2. Дело в том, что у вас скорее всего Community версия. В версии Ultimate диаграмы строются комбинациями Ctrl+Alt+U или Ctrl+Alt+Shift+U

      Удалить
    3. Спасибо, именно это мне и нужно. Поставил уже ультимейт )

      Удалить
  5. Подскажите, пожалуйста. Я не волшебник, я еще только учусь. Объект, какого из классов и интерфейсов, созданных вами, мне вызывать в методе мэйн, чтобы работать с дао? и какие параметры указывать? Я сделал дао по вашему образцу, в тестах все работает. но я не знаю через какой объект мне его использовать... Вызывать операции CRUD...

    ОтветитьУдалить
    Ответы
    1. Вначале вы должны создать фабрику, а потом через нее создавать Dao объекты для той сущности, над которой планируете выполнять CRUD операции. Примерно так:

      DaoFactory factory = new MySqlDaoFactory();
      Connection connection = factory.getContext();
      GenericDao dao = factory.getDao(connection, Group.class);

      Удалить
    2. Огромное спасибо за ответ! Статья очень помогла в изучении java, и, в частности, в изучении DAO паттерна. Автор молодец! Так держать!

      Удалить
  6. Этот комментарий был удален автором.

    ОтветитьУдалить
  7. Спасибо, все работает. Правда, в файл DaoFactory затесались "очепятки". Вот скачанный код:
    ********************************
    package ru.dokwork.daotalk.dao;


    import java.sql.Connection;
    import java.sql.SQLException;

    /** Фабрика объектов для работы с базой данных */
    public interface DaoFactory {

    public interface DaoCreator {
    public GenericDao create(Context context);
    }

    /** Возвращает подключение к базе данных */
    public Context getContext() throws PersistException;

    /** Возвращает объект для управления персистентным состоянием объекта */
    public GenericDao getDao(Context context, Class dtoClass) throws PersistException;
    }
    *****************************************
    Если "Context" заменить на "Connection", то все работает.
    Еще раз спасибо .

    ОтветитьУдалить
    Ответы
    1. Прочитайте внимательно статью. Это не опечатка.

      Удалить
  8. А можете подсказать что значит
    import ru.dokwork.daotalk.domain.Group;
    ?
    Я работаю в Eclipse, а код похоже был написан в Intellij.
    Что такое Group, откуда оно берётся, что там и как его подключить?

    ОтветитьУдалить
    Ответы
    1. Group - это класс, представляющий группу студентов. Вся концепция демонстрируется на примере отношения объектов Студент и Группа
      http://www.dokwork.ru/2014/02/daotalk.html#return_to_dao

      Удалить
    2. Извините, а можно пример описания этого класса?

      Удалить
    3. В конце статьи есть ссылка на исходный код. В частности на интересующий Вас класс: https://bitbucket.org/dok/daotalk/src/bbe5f2efb7863cc292d7d5e7e526f7ae2c15a3f3/src/main/java/ru/dokwork/daotalk/domain/Group.java

      Удалить
  9. Добрый день, подскажите пожалуйста. Я как понимаю в вашем коде запись в базу данных выполняется с помощью dao.create() в котором запускается конструктор по умолчанию для создания объекта(группы). А как же выполнить заполнение полей до записи?

    ОтветитьУдалить
    Ответы
    1. Вы можете самостоятельно создать и инициализировать объект, а затем сохранить его в базу методом dao.persist()

      Удалить
    2. Точно, очевидно ж вроде. Смутил метод create(), не очень понятно для чего он (зачем сохранять объект инициализируемый конструктором по умолчанию)

      Удалить
  10. Здравствуйте.
    Интересует вопрос, а что делать если мне нужны какие-то составные данные, к примеру результат селекта десятка таблиц (я не преувеличиваю, у нас сложная логика работы с бд). Ну и понятно что это будет исключительно выборка, т.е. делать Data Object (aka Value Object, aka Entity) под каждую выборку?

    ОтветитьУдалить
    Ответы
    1. Данный подход предполагает что да, под каждую выборку свой объект. Но если речь идет только о выборке, описанное в статье решение может быть оверхедом.

      Удалить
    2. В простых случаях мы используем DAO и это здорово, мы получаем все плюшки - более простой код, сокрытие логики работы с бд... Но в тоже время есть множество информации составного типа выборки из view и таблиц, сложные выборки со всяческой аналитикой, отчеты и прочее. Если DAO для таких случаев оверхед, то как вообще быть? В половине случаев использовать DAO, а в другой половине нет? Каким-то хитрым образом хотелось бы и в сложных случаем пользоваться всеми плюсами паттерна.

      Удалить
    3. Я имел в виду не сам паттерн, а конкретно решение. Главное его преимущество - переиспользование процесса отображение реляционного результата на объектное представление конкретной сущности. При этом предлагается заводить ДАО для каждого объекта отдельно. В вашем случае возможно что будет достаточно одного класса, методы которого будут выполнять запрос и отображение его результатов на объекты без использования AbstractJDBCDao.

      Удалить
    4. Спасибо за комментарии, да так будет лучше. И если можно еще ряд вопросов.
      1) Предполагается, что так называемый объект Entity не содержит что либо кроме полей-свойств таблицы и геттеры/сеттеры или там может быть что-то еще? Ну скажем какая-то картинка специфичная для конкретной сущности, например для сущности студента я хочу добавить поле типа const ICON = "/img/student.png" или ей там не место и надо этот вопрос решать на каком-то другом уровне абстракции?
      2) Объект DAO всегда работает с одной сущностью? Если нет, то как вы группируете логику?
      Приведу пример, вот у вас есть студенты с ними может быть еще много чего связано - таблицы оценок, посещаемости и прочее. И есть например в то же время в этой же БД есть какая-то логически малосвязная структура, ну например бухгалтерия... В таком случае как вы отделяете студентов и бухгалтерию?

      Удалить
    5. На мой взгляд оба вопроса крайне субъективны, но раз спросили, вот мой взгляд:
      1) Я бы не стал зашивать пути к файлам с иконками в таблицу, тк они не имеют отношения к доменной области и могут быть неоднократно изменены и перенесены, а то и вовсе вы решите от картинок отказаться :)
      2) Да, для несвязных сущностей разные DAO

      Удалить
  11. 1) Ну хорошо не в таблицу, а в DAO? В статье оригинала сказано, что хранилищем может выступать что угодно, а не только БД, файловая система например.

    2) А близкие по смыслу DAO вы как-то группируете? Может еще какой-то сервис поверх DAO? Чтобы вся работа со студентами и прилегающими объектами координировалась некоторым единым объектом?

    ОтветитьУдалить
    Ответы
    1. 2) никак :) на практике чаще используется ORM, и в силу того, что там поддерживаются отношения многие-ко-многим, группировать дао не приходится. Достаточно использовать только те, которые нужны конкретному сценарию бизнеслогики. Но в любом случае городить что-то для объединения дао я бы не стал и на практике присмотрелся к более гибким инструментам вроде ORM или чего-то на подобие Slick.

      1) Вообще, дао - прослойка над персистентным слоем. И, как правило, вы либо работаете через него с файлом, либо с базой, либо с чем-то еще. По поводу вашего примера, возможно есть смысл завести абстракцию для хранения изображения и в User хранить идентификатор картинки, по которому ее можно будет легко найти. Тогда изменение способа хранения изображений не затронет классы доменной области. А возможно стоит не городить огород и сделать так, как Вы написали выше и просто зашить пути в таблицу. Извечный вопрос баланса гибкости и простоты :)

      Удалить
  12. отличная статья, спасибо вам большое!
    Единственный вопрос - я правильно понял, что connection к БД нигде не закрываются?

    ОтветитьУдалить
    Ответы
    1. они закрываются там, где открываются, т.е. за рамками паттерна.

      Удалить
  13. Хорошо: мы убрали зависимость StudentDAO от GroupDAO, и вытягиваем нужный нам Group через getDependence(Group.class, Key).
    Но эта зависимость никуда не ушла: она просто перекочевала в наш DaoFatory. Если у нас будет 1000 обьектов, то наша DaoFatory превратиться в божественный обьект, а это антипаттерн, причем более плохой, чем зависимость StudentDAO от GroupDAO.

    ОтветитьУдалить
    Ответы
    1. Куча сущностей родит кучу ДАО, это верно. Фабрику тоже не обязательно делать одну. Но даже если она останется одной, это не обязательно будет Божественный объект. В сущности, на нее возлагается вполне определенная обязанность - инстанцирование ДАО и внедрение зависимости. Всегда можно прибегнуть к рефлекшену и унифицировать этот процесс.

      Удалить
  14. Все равно мне не очень нравится второй пример. Я в него сначала особо не вникал - но тут рядом чел сидит, который с ним разбирается - типа, единственный пример найденый в интернете без хибернейтов и прочих либ. И периодически этот чел у меня спрашивает...Когда шел первый пример - у него вроде вопросов не было, а потом пошел второй. И вобщем, мы подумали, и я решил написать еще о том, что не понравилось.

    Первое - зачем параметризировать DaoFactory коннекшеном? Коннекшен это же абстракция уровня базы - ее может или быть несколько, или может вообще это нетрадиционная база какая-то, без коннекшенов - может мы из CSV-файлов читаем напрямую, как пример. Сюда же - мы долго втыкали что за Context в параметре getDao интерфейса DaoFactory, и где же этот класс. Было бы понятней, если б там было T, а не Context - было б очевидно, что им параметризовано. Короче мне кажется, ненадо DaoFactory ничем параметризовать. Но если какой-то интерфейс уже параметризируешь - то в описании интерфейса надо писать не имя класса, а букву, а то неясно где там конкретный класс а где параметр.

    Второе - поскольку коннекшен - это абстракция более низкая чем dao-обьект, то пользовательский код не должен ничего знать о коннекшенах. Он может знать только что-то о dao-обьектах, и, возможно, допустимы коннекшены в сетапе - раз уж у вас юниттест является примером использования. Я смотрю класс RelationTest и вижу, что сначала в сетапе вы достаёте connection из MysqlFactory, а потом когда из этой же MysqlFactory получаете конкретный Dao-обьект - то параметром указываете этот же коннекшен! Нахрена? Если фабрика уже его знает - зачем ей его снова передавать, а если фабрике нужен новый коннекшен - зачем в setUp получать? А если юниттест не является примером использования - то где пример как всем этим пользоваться?

    Вобщем, материал как минимум для 3 статьи можно сказать - есть.

    И еще, если будете делать статью - стоит побаловаться с генериками так, чтобы в строчках типа:
    Group group = (Group) factory.getDao(connection, Group.class).create();
    не нужно было явно приводить к Group.

    ОтветитьУдалить
    Ответы
    1. Огромное спасибо за ответ.

      Давайте по порядку:
      Второй пример - это вы про продолжение статьи? Если да, то тут увы. Продолжение - это скорее рассуждение вслух, чем что-то готовое и рабочее.
      По поводу параметризации: именование - вопрос вкуса. Возможно Вы правы и буква более привычна в данном контексте, но в любом случае это не так принципиально. Гораздо важнее то, что я, видимо, не смог донести идею этого параметра. Как вы верно заметили, хранилеще - не обязательно реляционное СУБД, это может быть и файл. В любом случае должен быть некоторый контекст, в рамках которого идет сохранение или чтение данных. Для РСУБД - это connection, для файла - stream и т.д. В примере для статьи это именно подключение к базе. Открывать и закрывать подключение внутри методов ДАО нельзя, тк может потребоваться выполнить несколько операций в одной транзакции. Отсюда идея - вынести управление подключениями и транзакциями вовне, и создавать ДАО для каждого сеанса работы с базой.
      Ну и наконец генерики. Это да, это моя ошибка исправить которую никак руки не дойдут)

      Удалить
  15. Этот комментарий был удален автором.

    ОтветитьУдалить
  16. Подскажите как добавить запись в таблицу c помощью dao.create(),что нужно отправить в качестве параметра,если в методе он принимает T object? Я хочу вставить запись ("1","piter","fais")

    ОтветитьУдалить
    Ответы
    1. Если говорить в контексте данной статьи, то Т - это тип сущности которой оперирует DAO. Т.е. у вас должен быть реализован класс сущности с полями, соответствующими Вашим ("1","piter","fais") (Студент из примера в статье), для которого Вы должны реализовать Дао с методом persist, принимающим Ваши сущности (MySqlGroupDao из примера в статье).

      Удалить
  17. здраствуйте! можете по подробнее обьяснить про Generic и AdstractJdbcDao для чего они преднозначены?если не трудно,заранее спасибо

    ОтветитьУдалить
    Ответы
    1. привет! Ключевое понятие для глубокого понимания - параметрический полиморфизм. Можно почитать на вики: https://goo.gl/zyhqEd. Общая идея заключается в том, чтобы обобщить некоторый алгоритм для аргументов определенного множества типов. Самый простой пример - это список List. Каким бы ни был тип T, логика работы с его экземплярами в списке не изменится. То же самое и с AdstractJdbcDao: каким бы ни была сущность, получаемая из базы, логика работы с бд будет одинаковой.

      Удалить
  18. Долго вкуривал данный гайд по DAO и вот у меня возник вопрос, а как быть, если нужны специфические запросы к каждой таблице? Вот у меня есть 7 таблиц и для каждой из-них надо сделать дополнительно по 2-7 специфических запросов. Надо создавать специфические интерфейсы и имплементить их в классы, дописывая необходимый функционал в xxxDaoImpl. Поясните тогда в чём выигрыш от вашей реализации??

    ОтветитьУдалить
    Ответы
    1. Если результат запросов каждый раз разный, возможно что и выигрыша нет. Но идею абстрагироваться от слоя доступа к базе это не отменяет. Вам все равно надо возвращать результат запросов в объектном представлении и дописывать интерфейсы и имплиментацию дао.

      Удалить
  19. Мне кажется, Вы зря выносите Connection как поле.
    В каждом методе, должен быть свой Connection, Statement и ResultSet (где нужно).
    Если connection будет полем, возникнут ошибки с многопоточностью. Что вы можете сказать насчет этого ?

    ОтветитьУдалить
    Ответы
    1. На счет connection я с Вами полностью соглашусь. Сегодня я сделал бы его аргументом метода. На счет Statement и ResultSet не уверен, тк слишком уж мало инкапсуляции останется в методах.

      Удалить
  20. Этот комментарий был удален автором.

    ОтветитьУдалить
  21. Этот комментарий был удален автором.

    ОтветитьУдалить
  22. Этот комментарий был удален автором.

    ОтветитьУдалить
    Ответы
    1. и еще вопрос по поводу закрытия конекшенов, возможно стоит не получать конекшен из фабрики, а открывать закрывать внутри метода?

      Удалить
    2. Открытие Connection это "дорогая" операция. Крайне расточительно делать постоянное открытие и закрытие. Как минимум один Connection на один DAO и работа через пул конекшенов. Или один Connection на программу, но не многопоточную.

      Удалить
  23. Этот комментарий был удален автором.

    ОтветитьУдалить
  24. Здравствуйте.
    Подскажите, пожалуйста - как получить доступ на просмотр кода на Bitbucket?
    Буду очень признателен, если у меня появится возможность взглянуть на исходники!

    ОтветитьУдалить
    Ответы
    1. bitbucket прекратили поддержку hg, некоторые ссылки сломались, но код все еще доступен https://bitbucket.org/dok/daotalk/

      Удалить
    2. к сожалению, по этой ссылке пройти не могу - пишет:
      у вашего аккаунта нет доступа к этому контенту приложения Bitbucket.

      Удалить
    3. да, Вы правы. Исправил, попробуйте еще раз

      Удалить

Ваше мнение мне искренне интересно. Смелее!

Технологии Blogger.