Статьи

Использование перехватчика Hibernate для установки свойств журнала аудита

Почти в каждом приложении, которое я сделал, в таблицах базы данных есть какие-то поля контрольного журнала. Иногда это отдельная таблица «журнала аудита», в которую записываются все вставки, обновления, удаления и, возможно, даже запросы. Иногда есть четыре типичных поля журнала аудита в каждой таблице, например , вы могли бы иметь created_by, created_on, updated_byи updated_onполе в каждой таблице. Цель в последнем случае — обновить эти четыре поля соответствующей информацией о том, кто создал или обновил запись и когда они это сделали. Использование простого Hibernate Interceptorэто может быть выполнено без изменений в коде вашего приложения (с несколькими допущениями, которые я подробно опишу далее) Другими словами, вам не нужно и определенно не следует вручную устанавливать эти свойства аудита, замусоренные вокруг кода вашего приложения.

Основные предположения, которые я сделаю для этого простого перехватчика аудита, заключаются в том, что: (1) объекты модели содержат четыре свойства аудита, упомянутых выше, и (2) существует простой способ получения информации о текущем пользователе из любой точки кода. Первое предположение необходимо, так как вам нужен какой-то способ определить, какие свойства составляют свойства журнала аудита. Второе предположение является обязательным, поскольку вам нужен какой-то способ получения учетных данных лица, вносящего изменения, для установки свойства createdByили updatedByв вашем классе перехватчика Hibernate.

Итак, для справки предположим, что у вас есть базовая сущность (Groovy), подобная этой, с четырьмя свойствами аудита:

@MappedSuperclassclass BaseEntity implements Serializable {  String createdBy  Date createdOn  String updatedBy  Date updatedOn}

Я использую Hibernate ImprovedNamingStrategy, чтобы имена дел верблюдов были переведены в подчеркнутые имена, например, «createBy» становится «creation_by». Далее предположим, что существует BlogEntryкласс сущностей, который расширяет BaseEntityи наследует свойства журнала аудита:

@Entityclass BlogEntry extends BaseEntity {  @Id @GeneratedValue (strategy = GenerationType.IDENTITY)  Long id  @Version  Long version  String title  @Column (name = "entry_text")  String text  @Temporal (TemporalType.TIMESTAMP)  Date publishedOn}

Чтобы реализовать перехватчик, нам нужно реализовать вышеупомянутый интерфейс перехватчика . Мы могли бы сделать это напрямую, но лучше расширить EmptyInterceptor, поэтому нам нужно только реализовать методы, которые нам действительно нужны. Без лишних слов, вот реализация (исключая объявление пакета и импорт):

class AuditTrailInterceptor extends EmptyInterceptor {  boolean onFlushDirty(Object entity, Serializable id, Object[] currentState,                      Object[] previousState, String[] propertyNames,                      Type[] types) {    setValue(currentState, propertyNames, "updatedBy", UserUtils.getCurrentUsername())    setValue(currentState, propertyNames, "updatedOn", new Date())    true  }  boolean onSave(Object entity, Serializable id, Object[] state,                 String[] propertyNames, Type[] types) {    setValue(state, propertyNames, "createdBy", UserUtils.getCurrentUsername())    setValue(state, propertyNames, "createdOn", new Date())    true  }  private void setValue(Object[] currentState, String[] propertyNames,                        String propertyToSet, Object value) {    def index = propertyNames.toList().indexOf(propertyToSet)    if (index >= 0) {      currentState[index] = value    }  }}

Так что мы сделали? Во- первых, мы внедрили onFlushDirtyи onSaveметоды , потому что они вызываются для обновления SQL и вставок соответственно. Например, когда новая сущность впервые сохраняется, onSaveвызывается метод, и в этот момент мы хотим установить createdByсвойства и. И если существующий объект обновляется, onFlushDirtyвызывается и мы устанавливаем updatedByи updatedOn.

Во-вторых, мы используем setValueвспомогательный метод для реальной работы. В частности, единственный способ изменить состояние перехватчика Hibernate (о котором я все равно знаю) — это закопать currentStateмассив и изменить соответствующее значение. Для этого вам сначала нужно пройтись по propertyNamesмассиву, чтобы найти индекс свойства, которое вы пытаетесь установить. Например, если вы обновляете запись в блоге вы должны установить updatedByи updatedOnсвойство в пределах currentStateмассива. Для BlogEntryобъекта currentStateмассив может выглядеть следующим образом до обновления ( nullв этом случае обновленные свойства и свойства являются оба, потому что объект был создан Бобом, но еще не обновлен):

{   "Bob",   2008-08-27 10:57:19.0,   null,    null,    2008-08-27 10:57:19.0,    "Lorem ipsum...",   "My First Blog Entry",   0}

Затем вам нужно взглянуть на propertyNamesмассив, чтобы обеспечить контекст для того, что представляют вышеуказанные данные:

{  "createdBy",  "createdOn",  "updatedBy",  "updatedOn",  "publishedOn",  "text",  "title",  "version"}

Таким образом, в приведенном выше примере он updatedByнаходится с индексом 2 и updatedOnнаходится с индексом 3. setValue()работает, находя индекс свойства, которое необходимо установить, например «updatedBy», и, если свойство было найдено, он изменяет значение этого индекса в currentStateмассиве. , Таким образом, для updatedByиндекса 2 ниже приведен эквивалентный код, если мы фактически жестко закодировали реализацию, чтобы всегда ожидать, что поля аудита являются первыми четырьмя свойствами (что, очевидно, не очень хорошая идея):

// Equivalent hard-coded code to change "updatedBy" in above example// Don't use in production!currentState[2] = UserUtils.getCurrentUsername()

Чтобы реально заставить ваш перехватчик что-то делать, вам нужно включить его в Hibernate Session . Вы можете сделать это одним из нескольких способов. Если вы используете обычный Hibernate (то есть не с Spring или другой платформой), вы можете установить перехватчик глобально в SessionFactory или включить его для каждого, Sessionкак в следующем примере кода:

// Configure interceptor globally (applies to all Sessions)sessionFactory =  new AnnotationConfiguration()    .configure()    .setNamingStrategy(ImprovedNamingStrategy.INSTANCE)    .setInterceptor(new AuditTrailInterceptor())    .buildSessionFactory()// Enable per SessionSession session = getSessionFactory().openSession(new AuditTrailInterceptor())

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

<bean id="sessionFactory"      class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">  <property name="entityInterceptor">    <bean class="com.nearinfinity.hibernate.interceptor.AuditTrailInterceptor"/>  </property>  <!-- additional Hibernate configuration properties --></bean>

С другой стороны, если вы предпочитаете включить перехватчик для сеанса, вам нужно либо использовать метод openSession (Interceptor), чтобы открыть свои сеансы, либо альтернативно реализовать собственную версию CurrentSessionContext, чтобы использовать метод getCurrentSession () , чтобы установить перехватчик. Использование getCurrentSession()в любом случае предпочтительнее, поскольку позволяет нескольким разным классам (например, DAO) использовать один и тот же сеанс без необходимости явной передачи Sessionобъекта каждому объекту, который в этом нуждается.

На этом мы закончили. Но, если вы знаете о системе событий Hibernate (например, вы можете прослушивать такие события, как вставки и обновления и определять классы прослушивателей событий, чтобы реагировать на эти события), вам может быть интересно, почему я не использовал этот механизм, а не Interceptor. Причина в том, что, насколько мне известно , вы не можете изменять состояние объектов в прослушивателях событий. Так, например, вы не сможете изменить состояние объекта в PreInsertEventListenerклассе реализации. Если кто-то знает, что это неправильно или реализовал это, я хотел бы услышать об этом. До следующей встречи, счастливого одитинга!

Первоначально опубликовано в блоге Скотта Леберкнайта