Статьи

Весна — DAO и Сервисный уровень

Добро пожаловать в третью часть учебника Spring. В этой части мы продолжим написание нашего приложения расписания, а на этот раз мы внедрим слой DAO, бизнес-сервисы и напишем несколько тестов.

В предыдущей части мы определили интерфейс GenericDao, который сообщает нам, какие операции нам нужно будет выполнить над объектами. Теперь нам нужно обеспечить реализацию. Мы напишем класс, который выполняет эти операции в общем с помощью средств Hibernate (используя SessionFactory). Поэтому любой предоставленный DAO автоматически наследует эти основные операции. Мы поговорим об этом позже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package org.timesheet.service.impl;
 
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.timesheet.service.GenericDao;
 
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.util.List;
 
/**
 * Basic DAO operations dependent with Hibernate's specific classes
 * @see SessionFactory
 */
@Transactional(propagation= Propagation.REQUIRED, readOnly=false)
public class HibernateDao<E, K extends Serializable> implements GenericDao<E, K> {
 
    private SessionFactory sessionFactory;
    protected Class<? extends E> daoType;
 
    public HibernateDao() {
        daoType = (Class<E>) ((ParameterizedType) getClass().getGenericSuperclass())
                        .getActualTypeArguments()[0];
    }
 
    @Autowired
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }
 
    protected Session currentSession() {
        return sessionFactory.getCurrentSession();
    }
 
    @Override
    public void add(E entity) {
        currentSession().save(entity);
    }
 
    @Override
    public void update(E entity) {
        currentSession().saveOrUpdate(entity);
    }
 
    @Override
    public void remove(E entity) {
        currentSession().delete(entity);
    }
 
    @Override
    public E find(K key) {
        return (E) currentSession().get(daoType, key);
    }
 
    @Override
    public List<E> list() {
        return currentSession().createCriteria(daoType).list();
    }
}

Я хочу, чтобы вы отметили несколько вещей об этом коде:

  • Мы используем @Transcational аннотацию в верхней части класса. В основном это означает, что методы DAO будут выполняться в транскрипциях. Чтобы это работало, нам нужно изменить наш файл persistence-beans.xml и объявить там менеджер транзакций, который будет обрабатывать транзакции. Просто добавьте следующие строки (новое определение компонента):
    1
    2
    3
    4
    <bean id='transactionManager'
          class='org.springframework.orm.hibernate3.HibernateTransactionManager'>
        <property name='sessionFactory' ref='sessionFactory' />
    </bean>
  • Мы автоматически подключаем (@Autowired) SessionFactory, используя метод установки. Как вы должны знать, существует больше видов инъекций (поле, сеттер, конструктор). Инжекция поля лучше всего выглядит в Spring, потому что аннотация находится непосредственно в поле, а не в методе конструктора или установщика. С другой стороны, внедрение полей является наиболее бесполезным, потому что мы не можем вручную установить другие зависимости для частных полей (например, в модульном тесте). Я предпочитаю внедрение конструктора всякий раз, когда могу, потому что мне не нужно использовать мутатор (сеттер) для зависимости. Поэтому объекты строятся более безопасным способом. В этом конкретном случае мы будем использовать инъекцию сеттера, потому что мы проектируем этот класс для расширения. Если бы мы выбрали внедрение конструктора, все расширяющиеся классы должны иметь конструктор, соответствующий одному из суперкласса.

    Если вы хотите узнать больше об этом, я рекомендую эту замечательную книгу, написанную Дханджи Р. Прасанной.
    Также обратите внимание, что первая строка конструктора делает некоторую отражающую магию. Это потому, что Java не имеет обобщений во время выполнения, только во время компиляции, поэтому она не позволяет нам писать что-то вроде E.class . Поэтому мы использовали этот уродливый хак.

Теперь у нас есть базовый шаблон для операций DAO. В реальных системах обычно существует DAO для каждого объекта. Это потому, что иногда этих унаследованных CRUD-операций недостаточно, и вам нужны дополнительные бизнес-операции . Мы определим интерфейсы (наборы операций для каждого DAO), которые безопасны для типов, и мы будем зависеть только от тех, которые будут позже в контроллерах. Мы внедрим их с помощью Hibernate и сделаем так, чтобы они были подключены автоматически. Создайте новый пакет org.timesheet.service.dao и добавьте туда следующие интерфейсы — DAO для каждого объекта:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
package org.timesheet.service.dao;
 
import org.timesheet.domain.Employee;
import org.timesheet.service.GenericDao;
 
/**
 * DAO of employee.
 */
public interface EmployeeDao extends GenericDao<Employee, Long> {
 
    /**
     * Tries to remove employee from the system.
     * @param employee Employee to remove
     * @return {@code true} if employee is not assigned to any task
     * or timesheet. Else {@code false}.
     */
    boolean removeEmployee(Employee employee);
 
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
package org.timesheet.service.dao;
 
import org.timesheet.domain.Manager;
import org.timesheet.service.GenericDao;
 
/**
 * DAO of Manager.
 */
public interface ManagerDao extends GenericDao<Manager, Long> {
    /**
     * Tries to remove manager from the system.
     * @param manager Manager to remove
     * @return {@code true} if manager is not assigned to any task.
     * Else {@code false}.
     */
    boolean removeManager(Manager manager);
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
package org.timesheet.service.dao;
 
import org.timesheet.domain.Task;
import org.timesheet.service.GenericDao;
 
/**
 * DAO of Task.
 */
public interface TaskDao extends GenericDao<Task, Long> {
 
    /**
     * Tries to remove task from the system.
     * @param task Task to remove
     * @return {@code true} if there is no timesheet created on task.
     * Else {@code false}.
     */
    boolean removeTask(Task task);
 
}
01
02
03
04
05
06
07
08
09
10
11
package org.timesheet.service.dao;
 
import org.timesheet.domain.Timesheet;
import org.timesheet.service.GenericDao;
 
/**
 * DAO of Timesheet.
 */
public interface TimesheetDao extends GenericDao<Timesheet, Long> {
    // no additional business operations atm
}

Время для реализации. Мы просто расширим HibernateDao и реализуем соответствующий интерфейс. Нам нужны эти конкретные классы, потому что они будут внедрены в соответствующие поля (которые объявлены интерфейсом). Возможно, вы слышали что-то об этом подходе — это называется программирование для интерфейсов, и это то, что вы определенно хотите принять.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package org.timesheet.service.impl;
 
import org.hibernate.Query;
import org.springframework.stereotype.Repository;
import org.timesheet.domain.Employee;
import org.timesheet.service.dao.EmployeeDao;
 
@Repository('employeeDao')
public class EmployeeDaoImpl extends HibernateDao<Employee, Long> implements EmployeeDao {
 
    @Override
    public boolean removeEmployee(Employee employee) {
        Query employeeTaskQuery = currentSession().createQuery(
                'from Task t where :id in elements(t.assignedEmployees)');
        employeeTaskQuery.setParameter('id', employee.getId());
 
        // employee mustn't be assigned on no task
        if (!employeeTaskQuery.list().isEmpty()) {
            return false;
        }
 
        Query employeeTimesheetQuery = currentSession().createQuery(
                'from Timesheet t where t.who.id = :id');
        employeeTimesheetQuery.setParameter('id', employee.getId());
 
        // employee mustn't be assigned to any timesheet
        if (!employeeTimesheetQuery.list().isEmpty()) {
            return false;
        }
 
        // ok, remove as usual
        remove(employee);
        return true;
 
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package org.timesheet.service.impl;
 
import org.hibernate.Query;
import org.springframework.stereotype.Repository;
import org.timesheet.domain.Manager;
import org.timesheet.service.dao.ManagerDao;
 
@Repository('managerDao')
public class ManagerDaoImpl extends HibernateDao<Manager, Long> implements ManagerDao {
 
    @Override
    public boolean removeManager(Manager manager) {
        Query managerQuery = currentSession().createQuery(
                'from Task t where t.manager.id = :id');
        managerQuery.setParameter('id', manager.getId());
 
        // manager mustn't be assigned on no task
        if (!managerQuery.list().isEmpty()) {
            return false;
        }
 
        // ok, remove as usual
        remove(manager);
        return true;
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package org.timesheet.service.impl;
 
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.springframework.stereotype.Repository;
import org.timesheet.domain.Task;
import org.timesheet.domain.Timesheet;
import org.timesheet.service.dao.TaskDao;
 
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
 
@Repository('taskDao')
public class TaskDaoImpl extends HibernateDao<Task, Long> implements TaskDao {
 
    @Override
    public boolean removeTask(Task task) {
        Query taskQuery = currentSession().createQuery(
                'from Timesheet t where t.task.id = :id');
        taskQuery.setParameter('id', task.getId());
 
        // task mustn't be assigned to no timesheet
        if (!taskQuery.list().isEmpty()) {
            return false;
        }
 
        // ok, remove as usual
        remove(task);
        return true;
    }
 
    @Override
    public List<Task> list() {
        return currentSession().createCriteria(Task.class)
                .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
                .list();
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
package org.timesheet.service.impl;
 
import org.hibernate.Criteria;
import org.springframework.stereotype.Repository;
import org.timesheet.domain.Timesheet;
import org.timesheet.service.dao.TimesheetDao;
 
import java.util.List;
 
@Repository('timesheetDao')
public class TimesheetDaoImpl extends HibernateDao<Timesheet, Long> implements TimesheetDao {
 
    @Override
    public List<Timesheet> list() {
        return currentSession().createCriteria(Timesheet.class)
                .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
                .list();
    }
}

Я хочу, чтобы вы заметили, что у нас есть аннотация Spring @Repository для каждого класса DAO. Это потому, что мы не будем создавать их вручную, но будем вводить их и управлять ими в контейнере IoC Spring. Кстати, это более новый подход, ориентированный на аннотации. Конфигурация XML отсутствует, Spring выяснит это за нас ;) Мы можем использовать множество аннотаций, которые регистрируют классы как бины:

  • @Component — компонент автоматического сканирования (Spring bean)
  • @Repository — компонент в постоянном слое (обычно DAO)
  • @Service — компонент в слое обслуживания
  • @Controller — контроллер в архитектуре MVC

Еще один важный момент — мы передаем строковое значение в аннотацию @Repository. Я процитирую здесь Javadoc Spring, так как это самое ясное объяснение: «Значение может указывать на предложение для имени логического компонента, который будет превращен в bean-компонент Spring в случае автоопределенного компонента».

Теперь — время для тестирования! Создайте новый пакет: / src / test / java / org / timesheet / service / dao и поместите туда тесты.

Мы будем использовать некоторые внешние сценарии SQL для проверки состояния базы данных. Под src / main / resources создайте папку sql . Теперь давайте добавим два скрипта; cleanup.sql и create-data.sql . Теперь мы будем использовать только скрипт cleanup.sql, позже будет использован create-data.sql.

создание-data.sql

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
-- delete old data
delete from task_employee;
delete from timesheet;
delete from task;
delete from employee;
delete from manager;
 
-- add few employees
insert into employee values(1, 'management', 'Steve Jobs');
insert into employee values(2, 'management', 'Bill Gates');
insert into employee values(3, 'engineering', 'Steve Wozniak');
insert into employee values(4, 'engineering', 'Paul Allen');
 
-- add few managers
insert into manager values(1, 'Eric Schmidt');
insert into manager values(2, 'Steve Ballmer');
 
-- add some tasks
insert into task values(1, 0, 'task 1', 1);
insert into task values(2, 0, 'task 2', 2);
 
-- connect tasks to some employees
insert into task_employee values (1, 1);
insert into task_employee values (1, 3);
insert into task_employee values (1, 4);
insert into task_employee values (2, 2);
insert into task_employee values (2, 1);
 
-- create some timesheets on tasks
insert into timesheet values(1,
 5, -- hours
 1, -- first task
 1 -- employee steve jobs
);
 
insert into timesheet values(2,
 8, -- hours
 2, -- second task
 3 -- employee bill gates
);

cleanup.sql

1
2
3
4
5
delete from task_employee;
delete from timesheet;
delete from task;
delete from employee;
delete from manager;

Вам не нужно использовать мои данные; не стесняйтесь создавать некоторые по своему усмотрению. Просто убедитесь, что они имеют какой-то смысл для вас.

Прежде чем писать тест, нам нужен новый Spring bean. Он называется jdbcTemplate и хорошо известен для работы с JDBC в Spring. Это в основном оболочка для простого JDBC, которая упрощает многие вещи. Благодаря этому мы можем запустить скрипт простым вызовом, как вы увидите позже.

А пока добавьте этот bean-компонент в свой файл Spring Config persistence-beans.xml :

1
2
3
4
<bean id='jdbcTemplate'
        class='org.springframework.jdbc.core.simple.SimpleJdbcTemplate'>
    <constructor-arg type='javax.sql.DataSource' ref='dataSource'/>
</bean>

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

  • Если что-то было добавлено, проверьте, можете ли это найти
  • Если что-то было удалено, проверьте, что мы больше не можем это найти
  • Добавьте пару элементов в базу данных, подсчитайте их и убедитесь, что они были добавлены
  • Обновить элемент, сохранить его. Найдите это и проверьте, что это было изменено

Мне нравится думать о таких тестах, как интеграционные тесты, больше как модульные тесты. В огромном домене подобные тесты потребуют (в отличие от простых модульных тестов) довольно большого количества времени для запуска. На этот раз мы создадим специальный базовый класс с именем org.timesheet.DomainAwareBase . Это расширяет AbstractJUnit4SpringContextTests, так что мы можем автоматически подключать наши DAO, но также удаляет все данные из базы данных, прежде чем любой метод тестирования будет выполнен с использованием deleteScript.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package org.timesheet;
 
import org.junit.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import org.springframework.test.jdbc.SimpleJdbcTestUtils;
 
/**
 * Base makes sure that before any test empty database is available.
 */
@ContextConfiguration(locations = {'/persistence-beans.xml'})
public abstract class DomainAwareBase extends AbstractJUnit4SpringContextTests {
 
    private final String deleteScript = 'src/main/resources/sql/cleanup.sql';
 
    @Autowired
    private SimpleJdbcTemplate jdbcTemplate;
 
    @Before
    public void deleteAllDomainEntities() {
        SimpleJdbcTestUtils.executeSqlScript(jdbcTemplate,
                new FileSystemResource(deleteScript), false);
    }
}

Об автопроводке и оснастке:

Для меня оснастка особенно важна при автопроводке бобов. Это немного трудно перемещаться по коду без какой-либо дополнительной поддержки. Например, если вы используете окончательную редакцию IntelliJ IDEA, вы можете перейти непосредственно от поля к зависимостям с автосвязью, потому что IntelliJ добавит небольшой маркер.

Или вы также можете видеть автосвязанные зависимости вместе с объявленными в XML в представлении зависимостей.

Давайте посмотрим код для тестов сейчас:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package org.timesheet.service.dao;
 
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.timesheet.DomainAwareBase;
import org.timesheet.domain.Employee;
import org.timesheet.domain.Manager;
import org.timesheet.domain.Task;
import org.timesheet.domain.Timesheet;
 
import java.util.Arrays;
import java.util.List;
 
import static org.junit.Assert.*;
 
@ContextConfiguration(locations = '/persistence-beans.xml')
public class EmployeeDaoTest extends DomainAwareBase {
 
    @Autowired
    private EmployeeDao employeeDao;
 
    @Autowired
    private ManagerDao managerDao;
 
    @Autowired
    private TaskDao taskDao;
 
    @Autowired
    private TimesheetDao timesheetDao;
 
    @Test
    public void testAdd() {
        int size = employeeDao.list().size();
        employeeDao.add(new Employee('test-employee', 'hackzorz'));
 
        // list should have one more employee now
        assertTrue (size < employeeDao.list().size());
    }
     
    @Test
    public void testUpdate() {
        Employee employee = new Employee('test-employee', 'hackzorz');
        employeeDao.add(employee);
        employee.setName('updated');
 
        employeeDao.update(employee);
        Employee found = employeeDao.find(employee.getId());
        assertEquals('updated', found.getName());
    }
 
    @Test
    public void testFind() {
        Employee employee = new Employee('test-employee', 'hackzorz');
        employeeDao.add(employee);
 
        Employee found = employeeDao.find(employee.getId());
        assertEquals(found, employee);
    }
 
    @Test
    public void testList() {
        assertEquals(0, employeeDao.list().size());
         
        List<Employee> employees = Arrays.asList(
                new Employee('test-1', 'testers'),
                new Employee('test-2', 'testers'),
                new Employee('test-3', 'testers'));
        for (Employee employee : employees) {
            employeeDao.add(employee);
        }
 
        List<Employee> found = employeeDao.list();
        assertEquals(3, found.size());
        for (Employee employee : found) {
            assertTrue(employees.contains(employee));
        }
    }
 
    @Test
    public void testRemove() {
        Employee employee = new Employee('test-employee', 'hackzorz');
        employeeDao.add(employee);
 
        // successfully added
        assertEquals(employee, employeeDao.find(employee.getId()));
 
        // try to remove
        employeeDao.remove(employee);
        assertNull(employeeDao.find(employee.getId()));
    }
 
    @Test
    public void testRemoveEmployee() {
        Manager manager = new Manager('task-manager');
        managerDao.add(manager);
 
        Employee employee = new Employee('Jaromir', 'Hockey');
        employeeDao.add(employee);
 
        Task task = new Task('test-task', manager, employee);
        taskDao.add(task);
 
        Timesheet timesheet = new Timesheet(employee, task, 100);
        timesheetDao.add(timesheet);
 
        // try to remove -> shouldn't work
        assertFalse(employeeDao.removeEmployee(employee));
 
        // remove stuff
        timesheetDao.remove(timesheet);
        taskDao.remove(task);
 
        // should work -> employee is now free
        assertTrue(employeeDao.removeEmployee(employee));
    }
 
}
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
package org.timesheet.service.dao;
 
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.timesheet.DomainAwareBase;
import org.timesheet.domain.Employee;
import org.timesheet.domain.Manager;
import org.timesheet.domain.Task;
 
import java.util.Arrays;
import java.util.List;
 
import static org.junit.Assert.*;
 
@ContextConfiguration(locations = '/persistence-beans.xml')
public class ManagerDaoTest extends DomainAwareBase {
     
    @Autowired
    private ManagerDao managerDao;
 
    @Autowired
    private EmployeeDao employeeDao;
 
    @Autowired
    private TaskDao taskDao;
     
    @Test
    public void testAdd() {
        int size = managerDao.list().size();
        managerDao.add(new Manager('test-manager'));
 
        assertTrue (size < managerDao.list().size());
    }
 
    @Test
    public void testUpdate() {
        Manager manager = new Manager('test-manager');
        managerDao.add(manager);
        manager.setName('updated');
 
         managerDao.update(manager);
        Manager found = managerDao.find(manager.getId());
        assertEquals('updated', found.getName());
    }
 
    @Test
    public void testFind() {
        Manager manager = new Manager('test-manager');
        managerDao.add(manager);
 
        Manager found = managerDao.find(manager.getId());
        assertEquals(found, manager);
    }
     
    @Test
    public void testList() {
        assertEquals(0, managerDao.list().size());
         
        List<Manager> managers = Arrays.asList(
                new Manager('test-1'),
                new Manager('test-2'),
                new Manager('test-3')
        );
        for (Manager manager : managers) {
            managerDao.add(manager);
        }
 
        List<Manager> found = managerDao.list();
        assertEquals(3, found.size());
        for (Manager manager : found) {
            assertTrue(managers.contains(manager));
        }
    }
     
    @Test
    public void testRemove() {
        Manager manager = new Manager('test-manager');
        managerDao.add(manager);
         
        // successfully added
        assertEquals(manager, managerDao.find(manager.getId()));
         
        // try to remove
        managerDao.remove(manager);
        assertNull(managerDao.find(manager.getId()));
    }
 
    @Test
    public void testRemoveManager() {
        Manager manager = new Manager('task-manager');
        managerDao.add(manager);
 
        Employee employee = new Employee('Jaromir', 'Hockey');
        employeeDao.add(employee);
 
        Task task = new Task('test-task', manager, employee);
        taskDao.add(task);
 
        // try to remove -> shouldn't work
        assertFalse(managerDao.removeManager(manager));
 
        // remove task
        taskDao.remove(task);
 
        // should work -> no more tasks for manager
        assertTrue(managerDao.removeManager(manager));
    }
}
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package org.timesheet.service.dao;
 
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.timesheet.DomainAwareBase;
import org.timesheet.domain.Employee;
import org.timesheet.domain.Manager;
import org.timesheet.domain.Task;
 
import java.util.Arrays;
import java.util.List;
 
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
 
@ContextConfiguration(locations = '/persistence-beans.xml')
public class TaskDaoTest extends DomainAwareBase {
     
    @Autowired
    private TaskDao taskDao;
 
    @Autowired
    private ManagerDao managerDao;
 
    @Autowired
    private EmployeeDao employeeDao;
 
    @Test
    public void testAdd() {
        int size = taskDao.list().size();
 
        Task task = newSpringTask();
        taskDao.add(task);
 
        assertTrue(size < taskDao.list().size());
    }
 
    @Test
    public void testUpdate() {
        Task task = newSpringTask();
        taskDao.add(task);
         
        // update task
        task.setDescription('Learn Spring 3.1');
        taskDao.update(task);
 
        Task found = taskDao.find(task.getId());
        assertEquals('Learn Spring 3.1', found.getDescription());
    }
 
    @Test
    public void testFind() {
        Task task = newSpringTask();
        taskDao.add(task);
         
        assertEquals(task, taskDao.find(task.getId()));
    }
     
    @Test
    public void testList() {
        assertEquals(0, taskDao.list().size());
        Task templateTask = newSpringTask();
         
        List<Task> tasks = Arrays.asList(
                newTaskFromTemplate(templateTask, '1'),
                newTaskFromTemplate(templateTask, '2'),
                newTaskFromTemplate(templateTask, '3')
        );
        for (Task task : tasks) {
            taskDao.add(task);
        }
 
        List<Task> found = taskDao.list();
        assertEquals(3, found.size());
        for (Task task : found) {
            assertTrue(tasks.contains(task));
        }
    }
     
    @Test
    public void testRemove() {
        Task task = newSpringTask();
        taskDao.add(task);
         
        // successfully added
        assertEquals(task, taskDao.find(task.getId()));
         
        // try to remove
        taskDao.remove(task);
        assertNull(taskDao.find(task.getId()));
    }
 
    /**
     * @return Dummy task for testing
     */
    private Task newSpringTask() {
        Manager bob = new Manager('Bob');
        managerDao.add(bob);
 
        Employee steve = new Employee('Steve', 'Business');
        Employee woz = new Employee('Woz', 'Engineering');
        employeeDao.add(steve);
        employeeDao.add(woz);
 
        return new Task('Learn Spring', bob, steve, woz);
    }
 
    /**
     * Creates dummy task fo testing as copy of existing task and
     * adds aditional information to every field.
     * @param templateTask Task to copy
     * @param randomInfo Info to append everywhere
     * @return Random task for testing
     */
    private Task newTaskFromTemplate(Task templateTask,
            String randomInfo) {
        String description = templateTask.getDescription()
                + randomInfo;
         
        Manager manager = new Manager(
                templateTask.getManager().getName());
        managerDao.add(manager);
 
        List<Employee> templateEmployees = templateTask.getAssignedEmployees();
        Employee[] employees = new Employee[templateEmployees.size()];
 
        int idx = 0;
        for (Employee templateEmployee : templateEmployees) {
            Employee employee = new Employee(
                    templateEmployee.getName() + randomInfo,
                    templateEmployee.getDepartment() + randomInfo);
            employees[idx++] = employee;
            employeeDao.add(employee);
        }
 
        return new Task(description, manager, employees);
    }
}
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package org.timesheet.service.dao;
 
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.timesheet.DomainAwareBase;
import org.timesheet.domain.Employee;
import org.timesheet.domain.Manager;
import org.timesheet.domain.Task;
import org.timesheet.domain.Timesheet;
 
import java.util.Arrays;
import java.util.List;
 
import static org.junit.Assert.*;
 
@ContextConfiguration(locations = '/persistence-beans.xml')
public class TimesheetDaoTest extends DomainAwareBase {
 
    @Autowired
    private TimesheetDao timesheetDao;
 
    // daos needed for integration test of timesheetDao
    @Autowired
    private TaskDao taskDao;
 
    @Autowired
    private EmployeeDao employeeDao;
 
    @Autowired
    private ManagerDao managerDao;
 
    // common fields for timesheet creation
    private Task task;
    private Employee employee;
 
    @Override
    public void deleteAllDomainEntities() {
        super.deleteAllDomainEntities();
        setUp();
    }
 
    public void setUp() {
        employee = new Employee('Steve', 'Engineering');
        employeeDao.add(employee);
 
        Manager manager = new Manager('Bob');
        managerDao.add(manager);
 
        task = new Task('Learn Spring', manager, employee);
        taskDao.add(task);
    }
 
    @Test
    public void testAdd() {
        int size = timesheetDao.list().size();
 
        Timesheet timesheet = newTimesheet();
        timesheetDao.add(timesheet);
 
        assertTrue (size < timesheetDao.list().size());
    }
 
    @Test
    public void testUpdate() {
        Timesheet timesheet = newTimesheet();
        timesheetDao.add(timesheet);
 
        // update timesheet
        timesheet.setHours(6);
        taskDao.update(timesheet.getTask());
        timesheetDao.update(timesheet);
 
        Timesheet found = timesheetDao.find(timesheet.getId());
        assertTrue(6 == found.getHours());
    }
 
    @Test
    public void testFind() {
        Timesheet timesheet = newTimesheet();
        timesheetDao.add(timesheet);
 
        assertEquals(timesheet, timesheetDao.find(timesheet.getId()));
    }
 
    @Test
    public void testList() {
        assertEquals(0, timesheetDao.list().size());
        Timesheet templateTimesheet = newTimesheet();
         
        List<Timesheet> timesheets = Arrays.asList(
                newTimesheetFromTemplate(templateTimesheet, 4),
                newTimesheetFromTemplate(templateTimesheet, 7),
                newTimesheetFromTemplate(templateTimesheet, 10)
        );
        for (Timesheet timesheet : timesheets) {
            timesheetDao.add(timesheet);
        }
 
        List<Timesheet> found = timesheetDao.list();
        assertEquals(3, found.size());
        for (Timesheet timesheet : found) {
            assertTrue (timesheets.contains(timesheet));
        }
    }
 
    @Test
    public void testRemove() {
        Timesheet timesheet = newTimesheet();
        timesheetDao.add(timesheet);
         
        // successfully added
        assertEquals(timesheet, timesheetDao.find(timesheet.getId()));
         
        // try to remoce
        timesheetDao.remove(timesheet);
        assertNull (timesheetDao.find(timesheet.getId()));
    }
 
    /**
     * @return  Dummy timesheet for testing
     */
    private Timesheet newTimesheet() {
        return new Timesheet(employee, task, 5);
    }
 
    private Timesheet newTimesheetFromTemplate(Timesheet template,
            Integer hours) {
        return new Timesheet(
                template.getWho(),
                template.getTask(),
                hours
        );
    }
}

Вы можете запускать свои тесты как отдельные классы или все вместе из вашей IDE, или вы можете запускать их как «тестовую» цель из Maven, например, так (переключитесь на каталог проекта):

1
$ mvn test

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

Что касается DAO, мы почти закончили. Однако осталось одно — наш интерфейс TimesheetService. Это набор бизнес-операций, в которые мы вмешиваемся, поэтому давайте реализуем его с помощью Hibernate. Мы поместим класс TimesheetServiceImpl в пакет org.timesheet.service.impl :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package org.timesheet.service.impl;
 
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.timesheet.domain.Employee;
import org.timesheet.domain.Manager;
import org.timesheet.domain.Task;
import org.timesheet.service.TimesheetService;
import org.timesheet.service.dao.TaskDao;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
 
@Transactional(propagation= Propagation.REQUIRED, readOnly=false)
@Service('timesheetService')
public class TimesheetServiceImpl implements TimesheetService {
 
    // dependencies
    private SessionFactory sessionFactory;
    private TaskDao taskDao;
 
    private Random random = new Random();
 
    @Autowired
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }
 
    @Autowired
    public void setTaskDao(TaskDao taskDao) {
        this.taskDao = taskDao;
    }
 
    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }
 
    public TaskDao getTaskDao() {
        return taskDao;
    }
 
    private Session currentSession() {
        return sessionFactory.getCurrentSession();
    }
 
    @Override
    public Task busiestTask() {
        List<Task> tasks = taskDao.list();
        if (tasks.isEmpty()) {
            return null;
        }
         
        Task busiest = tasks.get(0);
        for (Task task : tasks) {
            if (task.getAssignedEmployees().size() > busiest.getAssignedEmployees().size()) {
                busiest = task;
            }
        }
         
        return busiest;
    }
 
    @Override
    public List<Task> tasksForEmployee(Employee employee) {
        List<Task> allTasks = taskDao.list();
        List<Task> tasksForEmployee = new ArrayList<Task>();
         
        for (Task task : allTasks) {
            if (task.getAssignedEmployees().contains(employee)) {
                tasksForEmployee.add(task);
            }
        }
 
        return tasksForEmployee;
    }
 
    @Override
    public List<Task> tasksForManager(Manager manager) {
        Query query = currentSession()
                .createQuery('from Task t where t.manager.id = :id');
        query.setParameter('id', manager.getId());
        return query.list();
    }
}

Обратите внимание, что на этот раз мы используем аннотацию @Service (мы говорили об этом раньше). Также мы вводим некоторые DAO с помощью сеттера . Некоторые бизнес-методы не реализованы наиболее эффективно, но мы демонстрируем, что мы можем либо смешивать общую логику DAO, либо создавать собственные запросы с использованием HQL. Мы могли бы выбрать Criteria API, сейчас это не имеет значения. Самым большим недостатком HQL является то, что это простые строки, поэтому он не удобен для рефакторинга — если вы не используете надлежащие инструменты. Например, IntelliJ имеет автозаполнение даже для простых строк. Просто выясняется, что вы пишете HQL. Довольно полезна также консоль HQL, у IntelliJ есть такая, и есть плагин для Eclipse .

Подсветка IntelliJ и автозаполнение для HQL:

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

Давайте поместим тестовый класс TimesheetServiceTest в пакет org.timesheet.service в папке src / test / java. В следующем коде обратите внимание, как мы используем bean-компонент jdbcTemplate:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package org.timesheet.service;
 
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import org.springframework.test.jdbc.SimpleJdbcTestUtils;
import org.timesheet.domain.Employee;
import org.timesheet.domain.Manager;
import org.timesheet.domain.Task;
import org.timesheet.service.dao.EmployeeDao;
import org.timesheet.service.dao.ManagerDao;
 
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
 
@ContextConfiguration(locations = '/persistence-beans.xml')
public class TimesheetServiceTest extends AbstractJUnit4SpringContextTests {
     
    @Autowired
    private TimesheetService timesheetService;
 
    // resources for accessing data during the testing
    @Autowired
    private SimpleJdbcTemplate jdbcTemplate;
    @Autowired
    private EmployeeDao employeeDao;
    @Autowired
    private ManagerDao managerDao;
 
    private final String createScript = 'src/main/resources/sql/create-data.sql';
    private final String deleteScript = 'src/main/resources/sql/cleanup.sql';
 
    @Before
    public void insertData() {
        SimpleJdbcTestUtils.executeSqlScript(jdbcTemplate,
                new FileSystemResource(createScript), false);
    }
 
    @After
    public void cleanUp() {
        SimpleJdbcTestUtils.executeSqlScript(jdbcTemplate,
                new FileSystemResource(deleteScript), false);
    }
 
    @Test
    public void testBusiestTask() {
        Task task = timesheetService.busiestTask();
        assertTrue(1 == task.getId());
    }
     
    @Test
    public void testTasksForEmployees() {
        Employee steve = employeeDao.find(1L);
        Employee bill = employeeDao.find(2L);
         
        assertEquals(2, timesheetService.tasksForEmployee(steve).size());
        assertEquals(1, timesheetService.tasksForEmployee(bill).size());
    }
     
    @Test
    public void testTasksForManagers() {
        Manager eric = managerDao.find(1L);
        assertEquals(1, timesheetService.tasksForManager(eric).size());
    }
     
}

Хорошо, вот и все. Мы внедрили DAO и сервисный уровень. Это включает в себя довольно много кода, поэтому прежде чем продолжить, убедитесь, что ваша структура проекта выглядит следующим образом:

Ссылка: Часть 3 — DAO и уровень обслуживания от нашего партнера JCG Михала Вртиака в блоге vrtoonjava .