در حوزه ORM مفهوم Database Auditing به معنی دنبال کردن و ردیابی ، Log کردن event های مربوط به Persistence Entity میباشد مزایای استفاده از Database Auditing همانند استفاده از Source Version Control است که در اینجا به معرفی سه روش از Database Auditing میپردازیم
نخست استفاده از JPA استاندارد و بعد با استفاده از JPA و یکبار همراه با Hibernate و دیگری همراه با Spring Data خواهیم پرداخت
طی مثال ها دو کلاس Foo و Bar که entity های ما هستند را فرض کنید که کلاس Foo یک فیلد از نوع Bar دارد
Auditing With JPA:
JPA بتنهایی قابلیتی برای Auditing ندارد ولی ما با استفاده از امکانات موجود آنرا شبیه سازی میکنیم
Annotation های PrePersist ، @PreUpdate ، @PreRemove@ میتوانند روی متد هایی قرار بگیریند و نقش callback در عملیات insert , update , delete را بر عهده بگیرند این متد ها باید بدون آرگومان ورودی و مقدار برگشتی void داشته باشند و static نباشند
@Entity public class Bar { @PrePersist public void onPrePersist() { ... } @PreUpdate public void onPreUpdate() { ... } @PreRemove public void onPreRemove() { ... } }
* این متد ها نباید در lifecycle خود کوئری ای را اجرا کنند یا از EntityManager استفاده کنند یا به Entity دیگری دسترسی داشته باشند یا روابط را همان Persistence Context تغییر دهند
اگر برای Auditing از فریم ورک خاصی استفاده نمیکنیم باید Domain model و Database Schema را بصورت دستی نگهداری کنیم که برای ساده سازی ما دو ویژگی جدید را اضافه میکنیم یکی ذخیره کردن نام عملیات و دیگری زمان آن :
@Entity public class Bar { //... @Column(name = "operation") private String operation; @Column(name = "timestamp") private long timestamp; //... // standard setters and getters for the new properties //... @PrePersist public void onPrePersist() { audit("INSERT"); } @PreUpdate public void onPreUpdate() { audit("UPDATE"); } @PreRemove public void onPreRemove() { audit("DELETE"); } private void audit(String operation) { setOperation(operation); setTimestamp((new Date()).getTime()); } }
اگر چندین کلاس Entity وجود داشته باشد که نیاز به Auditing داشته باشد میتوانیم با استفاده از EntityListeners@ عملیات Auditing را در یک کلاس متمرکز کنیم :
public class AuditListener { @PrePersist @PreUpdate @PreRemove private void beforeAnyOperation(Object object) { ... } }
و در کلاس entity انرا معرفی کنیم :
@EntityListeners(AuditListener.class) @Entity public class Bar { ... }
Auditing با Hibernate :
برای Auditing در Hibernate میتوانیم از Interceptors و EventListeners استفاده کنیم اما Hibernate ماژول دیگری برای اینکار معرفی کرده است به نام Envers :
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-envers</artifactId> <version>${hibernate.version}</version> </dependency>
و کافی است در کلاس های entity از Audited@ استفاده کنیم و یا اگر نیاز بود روی Columns@ خاصی Auditing داشته باشیم همراه با آن استفاده کنیم
@Entity @Audited public class Bar { ... }
همانطور که در ابتدا توضیح داده شده هر Bar میتواند با تعدادی Foo رابطه داشته باشد (one to many) و باید در کلاس Foo هم از Audited@ استفاده کنیم و یا از NotAudited@ در رابط آن استفاده کنیم :
@OneToMany(mappedBy = "bar") @NotAudited private Set<Foo> fooSet;
ساخت جدولی برای لاگ گرفتن Audit ها :
برای ساخت این جدول راه هایی وجود دارد :
- ست کردن hibernate.hbm2ddl.auto به یک از مقادیر create , create-drop , update تا Envers بتواند آن جدول را اتوماتیک بسازد
- استفاده از org.hibernate.tool.EnversSchemaGenerator برای استخراج database schema با کدنویسی
- برای تولید DDL ها از Ant task استفاده کنیم
- با استفاده از یکی از پلاگین های Maven مانند Juplo اسکیمای دیتابیس ها را با توجه به روابط انها بسازیم و در envers استفاده کنیم
روش اول راحت ترین راه است ولی برای استفاده در محصول تجاری ایمن نیست
با این حال اگر ما کلاس Foo را Audited@ کنیم باید جداول bar_AUD و foo_AUD بصورت اتوماتیک ساخته شوند در این جداول کلیه فیلد های کلاس های entity و دو فیلد اضافه REVTYPE و REV وجود خواهد داشت که مقادیر REVTYPE میتواند 0 برای insert و 1 برای update و 2 برای delete ثبت شود
علاوه بر جدول اختصاص یافته به هر Entity یک جدول دیگر بنام REVINFO بصورت پیش فرض ساخته میشود که دو فیلد مهم در ان است REV و REVTSTMP که زمان را برای هر ورژن از entity ثبت میکند
و foo_AUD.REV و bar_AUD.REV کلید خارجی به REVINFO.REV هستند
تنظیمات Envers :
در این تنظیمات میتوانیم پیشوند جداول ساخته شده را تغییر دهیم
Properties hibernateProperties = new Properties(); hibernateProperties.setProperty( "org.hibernate.envers.audit_table_suffix", "_AUDIT_LOG"); sessionFactory.setHibernateProperties(hibernateProperties);
دیگر تنظیمات در این لینک قابل مطالعه است
دسترسی به تاریخچه Entity ها :
مشابه استفاده از Criteria API میتوانیم تاریخچه entity ها را با استفاده از اینترفیس AuditReader و کلاس سازنده آن AuditReaderFactory مشاهده کنیم :
AuditReader reader = AuditReaderFactory.get(session);
حال میتوانیم روی ابجکت موجود تاریخچه entity ها را ببینیم مثلا تغییرات در ورژن 2 که bar_AUDIT_LOG.REV = 2 است
AuditQuery query = reader.createQuery() .forEntitiesAtRevision(Bar.class, 2)
اگر بخواهیم لیستی از کلیه Entity در کلیه ورژن ها داشته باشیم میتوانی بدین صورت دسترسی پیدا کنیم :
AuditQuery query = reader.createQuery() .forRevisionsOfEntity(Bar.class, true, true);
اگر پارامتر دوم را false کنیم با جدول REVINFO نتیجه join میشود و اگر true باشد فقط Entity ها برگشت داده میشوند و پارامتر آخر مشخص میکند که آیا entity های حذف شده را هم برگرداند یا خیر
بعضی محدودیت ها را توسط AuditEntity میتوان اعمال کرد :
query.addOrder(AuditEntity.revisionNumber().desc());
Audit کردن با Spring Data JPA :
ابتدا آنرا با EnableJpaAuditing@ در کلاس Configuration@ فعال میکنیم :
@Configuration @EnableTransactionManagement @EnableJpaRepositories @EnableJpaAuditing public class PersistenceConfig { ... }
اضافه کردن کلاس Listener callback برای entity ها :
@Entity @EntityListeners(AuditingEntityListener.class) public class Bar { ... }
اضافه کردن فیلدهایی برای Tracking و ذخیره کردن تاریخ ایجاد و تاریخ تغییرات که مقادیر آنها اتوماتیک پر میشود :
@Entity @EntityListeners(AuditingEntityListener.class) public class Bar { //... @Column(name = "created_date", nullable = false, updatable = false) @CreatedDate private long createdDate; @Column(name = "modified_date") @LastModifiedDate private long modifiedDate; //... }
* ما متیوانیم یک کلاس پدر که با MappedSuperClass@ مشخص شده است داشته باشیم که کلیه Audited Entity های ما از آن ارث بری میکنند و این فیلد ها را به آنجا منتقل کنیم
ثبت Author تغییرات با Spring Security :
@Entity @EntityListeners(AuditingEntityListener.class) public class Bar { //... @Column(name = "created_by") @CreatedBy private String createdBy; @Column(name = "modified_by") @LastModifiedBy private String modifiedBy; //... }
نام Author میتواند از طریق Spring Security Context بدست آید ولی اگر نیاز داریم که این اسم را بصورت سفارشی وارد کنیم میتوانیم با استفاده از پیاده سازی اینترفیس AuditAware آنرا ثبت کنیم :
public class AuditorAwareImpl implements AuditorAware<String> { @Override public String getCurrentAuditor() { // your custom logic } }
و انرا به EnableJpaAuditing@ در قالب یک Bean معرفی کنیم پس باید یک Bean هم برای این منظور در نظر بگیریم که مقدار برگشتی آن از نوع AuditorAware باشد :
@EnableJpaAuditing(auditorAwareRef="auditorProvider") public class PersistenceConfig { //... @Bean AuditorAware<String> auditorProvider() { return new AuditorAwareImpl(); } //... }