RecordTracking unobtrusively tracks the creation, updating and elimination events regarding to records.
When one of those events occurs, writes into a log:
- The name of the action
- The date when the action was invoked
- The user who invoked the event
- The name of the model
- The model data including association id’s
Set the following properties in conf/application.conf
recordtracking.sessionKey=username
This propoerty is for obtaining the User from the session cookie.
If the application doesn’t use sessions, then user name is recorded as ==_UNKNOWN_==
If any data is loaded via yaml, then user name is recorded as ==_YAML_==
RecordTracking uses log4j in order to write data wherever you want, as long as you know how to manipulate log4j.
By default, appender and loggers must be configured in log4j.xml or log4j.properties.
RecordTracking first tries to read the xml file, if it doesn’t exist, then tries to read the properties file.
The logger for this module must be named recordtracking
- Add this module to your dependecy.yml file
- Configure log4j
If you don’t want a model data be tracked at all, then annotate such model with @NoTracking
@Entity
@NoTracking
public class MyModel extends Model {
...
}
If you want to mask a field’s value, then annotate such field with @Mask
@Entity
public class Woman extends Model {
@Mask
public Integer age;
}
Every single character of the value will be replaced by an * character
By default, the data model is formatted as follows:
- The string: -—[BEGIN]——-
- The event name
- The event date
- The user who triggered the event
- An empty line
- The model name
- The fields (preceded by @) and its values, including associations id’s
- The string: -—[ END ]——-
E.g.,
-----[BEGIN]----- POST REMOVE Thu Nov 24 11:02:20 CST 2011 User: elbarto <models.Quote> @author_id:3 @quotation:Never more @id:3 -----[ END ]----- -----[BEGIN]----- POST REMOVE Thu Nov 24 11:02:20 CST 2011 User: elbarto <models.Author> @last_name:Poe @quotes_ids:1 2 3 @id:1 @first_name:Alan @years:*** -----[ END ]-----
If you want another format for a particular model, then write your own formatRecordTracking
E.g.,
@Entity
public class Author extends Model {
Custom formatRecordTracking method
public String formatRecordTracking(String event) {
return String.format("%nevent%n%s%n", "YOU WILL KNOW NOTHING FROM ME, :p");
}
}
It enhances via byte-code all persistent models.
A transient field is injected:
@Transient Map track_data;
The following methods are injected:
- @PostPersit onPostPersist()
- @PreRemove onPreRemove()
- @PostRemove onPostRemove()
- @PreUpdate onPreUpdate()
- @PostUpdate onPostUpdate()
- formatRecordTracking(String event)
- _fill_track_data()
If there’s already a method annotated with @PostPersist, @PreRemove, @PostRemove, @PreUpdate, @PostUpdate, then
then the logic is just inserted at the beginning of the annotated method, depending on the case.
When a PRE-Event is triggered the data to be record is stored into a transient Map (track_data), afterwards when
a POST-Event is triggered the formatRecordTracking method is invoked which in turns invokes log4j so that the data is being recorded.
Java implements generics with type erasure. The compiler uses this type information only for safety checks, the byte code doesn’t contain any information about generic types.
Consequently, the runtime doesn’t know about it either, so you cannot retrieve it by reflection.
When there’s a one-to-many, many-to-one or many-to-many relationship, the model having that association field, needs to specify explicitly the type of the collection.
By convention, that cast type must be named as follows:
collectionFieldName + CastType
E.g.,
public class Author extends GenericModel {
.
// REVERSE ASSOCIATIONS
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, fetch = FetchType.LAZY) // name of the variable in the other object that references this object
public List<Quote> quotes; // has_many :quotes
@Transient private Quote quotesCastType; // Due to type erasure, RecordTracking needs to be instructed which is the collection type
.
// FIELDS
@Id
@GeneratedValue
public Long pk;
}
If all models inherit from play.db.jpa.Model then the cast-type field is not needed
TODO: When enhancing, check if the class is an instance of play.db.jpa.Model
With the @OneToOne, @OneToMany and @ManyToOne annotations, JPA by default assigns EAGER FetchType, if you change this behavior to LAZY
the module won’t be able to delete the Object which owns the association.
I’m not very good at JPA and I don’t know why this behavior happens.
If any one knows I’d really appreciate the collaboration.
One sample demo is part of the distribution, there are log4j.xml and log4j.properties files included.
Don’t forget to run play deps so that it resolves dependencies.
This module uses a small API: play utilities
- Write into a single block all chained events triggered by a cascade event.
- Improve the sample demo
I tried to achieve the same functionality by events callback, but I have to modify two classes of the play framework.
Commit improvements to the Play Framework Core.
- PlayPlugin
- Add a preEvent() method
- JPABase
- _save() → invoke: PlayPlugin.preEvent(“JPASupport.objectPrePersisted”, this);
- _delete() → invoke: PlayPlugin.preEvent(“JPASupport.objectPreDeleted”, this);
Author: Omar O. Román