6 декабря 2013 в 15:24

Spring и обработка событий в Hibernate tutorial

Как-то обделена на хабре такая тема, как обработка событий при работе с сущностями с использованием Hibernate — я смог найти только один пост уже почти мохнатого года. Но то аудит, а нам нужна возможность автоматизировать работу с некоторыми атрибутами сущностей и при этом упростить процедуру работы с ними.

Для начала создадим демонстрационный стенд с двумя сущностями User и AnObject, а так же DAO-слоем для них.
Код
Здесь и далее привожу только значимые куски кода — в полной версии можно посмотреть на github
@Entity
@Table(name = "user")
public class User {
        @Id
        @GeneratedValue
        private long id;
        @Basic
        @Column(name = "username", updatable = false, unique = true, nullable = false)
        private String username;

        // getter and setter
}

@Entity
@Table(name = "anObject")
public class AnObject {
        @Id
        @GeneratedValue
        private long id;
        @Column
        private String value;

        // getter and setter
}



Добавим в сущность AnObject атрибут с двумя свойствами — дата последнего редактирования и автор правки:
Код
@Embeddable
public class LastModified {
        @Column
        @Temporal(TemporalType.TIMESTAMP)
        private Calendar lastUpdated;
        @OneToOne
        @JoinColumn(name = "lastEditor_id")
        private User lastEditor;

        // getter and setter
}

public interface LastModifiable {
        LastModified getLastModified();

        void setLastModified(LastModified modified);
}

@Entity
@Table(name = "anObject")
public class AnObject implements LastModifiable {
        @Id
        @GeneratedValue
        private long id;
        @Column
        private String value;
        @Embedded
        private LastModified lastModified;

        // getter and setter
}


И не забудем поправить тестовый класс с учётом нововведений.

С этого момента у нас уже есть всё необходимое для внесения данных о дате/авторе изменений в ручном режиме, но… люди мы «ленивые», поэтому давайте автоматизируем и эту работу — пусть за нас везде это делает Hibernate. Для этого добавляем Listener и просим Hibernate его использовать при возникновении события save или update (commit):
Код
@Component
public class LastModifiedListener extends DefaultSaveOrUpdateEventListener {
        private transient static final Logger LOG = LoggerFactory.getLogger(LastModifiedListener.class.getName());

        @Autowired
        private UserDao userDao;

        @Override
        public void onSaveOrUpdate(SaveOrUpdateEvent event) {
                LOG.trace("object: {}", event.getObject());
                if (event.getObject() instanceof LastModifiable) {
                        LastModified lastModified = new LastModified((User) userDao.get(2));
                        ((LastModifiable) event.getObject()).setLastModified(lastModified);
                        LOG.trace("object: {}", event.getObject());
                }
                super.onSaveOrUpdate(event);
        }
}

@Component
public class HibernateEventWiring {
        @Autowired
        private SessionFactory sessionFactory;

        @Autowired
        private LastModifiedListener lastModifiedListener;

        @PostConstruct
        public void registerListeners() {
                EventListenerRegistry registry = ((SessionFactoryImpl) sessionFactory).getServiceRegistry().getService(
                        EventListenerRegistry.class);

                registry.getEventListenerGroup(EventType.SAVE_UPDATE).prependListener(lastModifiedListener);
        }
}



Теперь остался заключительный штрих — поправить тесты так, чтобы они учитывали новые изменения (хотя правильнее было бы сперва тесты поправить, а потом уже добавлять Listener)

На этом можно поставить финальную точку — у любой сущности, которая имплементирует интерфейс LastModifiable автоматически при каждом сохранении в БД будут изменяться поля lastUpdated и lastEditor.

UPD: В примере была небольшая недосказанность — прослушивание было только для события saveOrUpdate, в то время, как могут быть вызваны и просто save и просто update. обновил тесты
UPD: Обновил исходники — добавил пример с использованием Spring Data JPA (использовал только сущности, без слушателей). Слушателя org.springframework.data.jpa.domain.support.AuditingEntityListener не стал добавлять, чтобы сохранился пример с обработкой событий в Hibernate
Виктор Аленьков @Borz
карма
22,7
рейтинг 7,3
Java Senior Developer
Похожие публикации
Самое читаемое Разработка

Комментарии (17)

  • 0
    UPD: В примере была небольшая недосказанность — прослушивание было только для события saveOrUpdate, в то время, как могут быть вызваны и просто save и просто update. обновил тесты
  • +1
    Или можно воспользоватся аннотациями к примеру обновлять дату создания и модификации модели.

    @MappedSuperclass
    public abstract class AbstractEntity<PK extends Serializable> extends AbstractPersistable<PK> {
    	
    	private static final long serialVersionUID = 1L;
    	
    	@Column(nullable = false)
    	private Date createdDate;
    	
    	@Column(nullable = false)
    	private Date updatedDate;
    	
    	@PrePersist
    	public void onCreate() {
    		this.createdDate = new Date();
    		this.updatedDate = new Date();
    	}
    	
    	@PreUpdate
    	public void onUpdate() {
    		this.updatedDate = new Date();
    	}
    }
    


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

    	@PrePersist
    	public void onCreate() {
    		this.createdDate = new Date();
    		this.updatedDate = new Date();
    	}
    	
    	@PreUpdate
    	public void onUpdate() {
    		this.updatedDate = new Date();
    	}
    


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

      PS. Проблема реализации на родительском классе в том, что отнаследоваться можно только от одного класса.
      • 0
        Наследоваться не обязательно, как я написал можно прописывать всё в каждой моделе, но если вам нужно автоматом обновлять какие то поля как в Rails created_at и udated_at поля у каждой модели то лучше отнаследоваться чтобы не повторять код.

        Меня другое смущает как вы в событие передаёте юзера который делает изменение. Пока то что у вас в коде захардкоден юзер с id 2
        LastModified lastModified = new LastModified((User) userDao.get(2));
        
        • 0
          как-то так:
          final User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
          
          • 0
            Да про Spring Security с залогиненым юзером не подумал.
  • 0
    Наверное, есть случаи, где это удобно, но этот конкретный пример можно было сделать и на триггерах, упростив код.
    • 0
      я правильно понимаю — на триггерах в БД? тогда как бы вы получили текущего пользователя?
      • 0
        В оракле можно sys_context()(см. Context Demo) для этих целей использовать.
        • 0
          а разве у нас не один пользователь к БД подключается? вы же не предлагаете на каждого пользователя системы заводить отдельного пользователя БД?
          • 0
            вы же не предлагаете на каждого пользователя системы заводить отдельного пользователя БД?

            Конечно же нет. Пользователь, работающий с БД, один, просто после успешной авторизации в системе, выполняется pl/sql процедура, в которую передается идентификатор текущего пользователя. В самой процедуре выполняется

            dbms_session.set_context('MyApp', 'currentUserId', :user_id, USER)

            Как-то так, приблизительно.
            • 0
              мне кажется, или пример с использованием PS/SQL более конкретен чем мой, т.к. будет привязан к конкретной СУБД или к ограниченному кругу СУБД, поддерживающих конкретный синтаксис PL/SQL?
              • 0
                Ну, я вообще-то изначально говорил про Оракл и про то, как можно получить текущего пользователя приложения на стороне БД. Во-вторых, использование триггеров в любом случае это привязка к СУБД.
                • 0
                  триггер не обязательно должен быть в БД — мой пост тому пример.

                  и код, в случае с триггерами в БД, не упростится, а усложнится, т.к. будет включать в себя неявные «инъекции» внутри БД, влияющие на логику работы приложения.

                  Но это всё как карандаши/фломастеры/etc :)
                  • 0
                    В Вашем посте приведен пример слушателя. Триггер, конечно, тоже слушатель, но обычно, говоря «триггер» подразумевают его именно в БД.

                    З.Ы. Насчёт карандашей/фломастеров/etc полностью согласен
  • 0
    UPD: Обновил исходники — доработал пример с использованием Spring Data JPA (использовал только сущность org.springframework.data.jpa.domain.AbstractAuditable, без слушателей)
    • 0
      Слушателя org.springframework.data.jpa.domain.support.AuditingEntityListener не стал добавлять, чтобы сохранился пример с обработкой событий в Hibernate.

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