Почти в каждом приложении, которое я сделал, в таблицах базы данных есть какие-то поля контрольного журнала. Иногда это отдельная таблица «журнала аудита», в которую записываются все вставки, обновления, удаления и, возможно, даже запросы. Иногда есть четыре типичных поля журнала аудита в каждой таблице, например , вы могли бы иметь 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
классе реализации. Если кто-то знает, что это неправильно или реализовал это, я хотел бы услышать об этом. До следующей встречи, счастливого одитинга!
Первоначально опубликовано в блоге Скотта Леберкнайта