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
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 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.

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