Hibernate validator works well for me to validate objects fetched by hibernate, but the problem is that I would like to make sure that certain conditions are met after persisting/updating objects in database. For example:
My condition is: User can host at most 3 games
Constraint annotation:
#Target({ FIELD, TYPE })
#Retention(RUNTIME)
#Constraint(validatedBy = GamesCountValidator.class)
#Documented
public #interface ConstrainHostGamesCount {
String message() default "{com.afrixs.mygameserver.db.constraint.ConstrainHostGamesCount.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
Validator:
public class GamesCountValidator implements ConstraintValidator<ConstrainHostGamesCount, User> {
#Override
public void initialize(ConstrainHostGamesCount constraintAnnotation) {
}
#Override
public boolean isValid(User user, ConstraintValidatorContext context) {
if (user == null)
return true;
return user.getGames().size() <= 3;
}
}
User class:
#Entity
#Table(name="Users")
#ConstrainHostGamesCount
public class User {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="id", nullable=false, unique=true, length=11)
private int id;
#Column(name="name", length=30, unique=true)
private String name;
#OneToMany(mappedBy = "user", orphanRemoval = true, fetch = FetchType.LAZY)
private Set<Game> games = new HashSet<>();
//generic getters and setters
}
Game class:
#Entity
#Table(name="Games")
public class Game {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="id", nullable=false, unique=true, length=11)
private int id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="user_id")
#ConstrainHostGamesCount
private User user;
//generic getters and setters
}
Test method:
public class Test {
public static void hostGames(String userName, int count) {
try {
Session session = DatabaseManager.getSessionFactory().getCurrentSession();
session.beginTransaction();
Query userQuery = session.createQuery("from User where name = :name");
userQuery.setParameter("name", name);
User user = (User)userQuery.uniqueResult();
for (int i = 0; i < count; i++) {
Game = new Game();
game.setUser(user);
session.persist(game);
}
session.getTransaction().commit();
} catch (Exception e) {
DatabaseManager.getSessionFactory().getCurrentSession().getTransaction().rollback();
e.printStackTrace();
}
}
}
Desired behavior for Test.hostGames("afrixs", 4) would be to fail. However the validator validates the state of the user object before the update, ie. with games.size() equal to 0, so the constraint condition is met and nothing fails until Test.hostGames("afrixs", 4) is called for the second time. Of course in this situation we could manually add games to user user.getGames().add(game) but this attitude is error prone (the game needs to be added to user this way everywhere in the code) and it doesn't solve the problem if for example two Test.hostGames("afrixs", 2) are called asynchronously.
So my question is: what is the proper way of constraining the database integrity using hibernate? Is there a way to make the validator check the final state of objects after storing them into database? Or do I need to do the constraining manually (like performing another transaction after session.getTransaction().commit and check the conditions and roll back the updating transaction if they are not met)? Or should I leave out hibernate and use SQL triggers for this? Thank you for your answers, they will help a lot
And here is my current hibernate validation configuration:
<property name="javax.persistence.validation.group.pre-persist">javax.validation.groups.Default</property>
<property name="javax.persistence.validation.group.pre-update">javax.validation.groups.Default</property>
<property name="hbm2ddl.auto">validate</property>
Ok, I have made some experiments, writing down a small test class. To make things simple I changed the constraint to "User can host at most 1 game".
public class DBTest {
#Test
public void gamesCountConstraintWorking() {
DBManager.deleteHostedGames("afrixs");
boolean ok1 = DBManager.createOneGame("afrixs");
boolean ok2 = DBManager.createOneGame("afrixs");
int gamesCount = DBManager.deleteHostedGames("afrixs");
System.out.println("Sync test: count: "+gamesCount+", ok1: "+ok1+", ok2: "+ok2);
assertTrue(gamesCount <= 1);
assertTrue(!(ok1 && ok2));
}
#Test
public void gamesCountConstraintWorkingAsync() throws InterruptedException {
DBManager.deleteHostedGames("afrixs");
for (int i = 0; i < 30; i++) {
CreateOneGameRunnable r1 = new CreateOneGameRunnable(1);
CreateOneGameRunnable r2 = new CreateOneGameRunnable(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
int maxCount = 0;
while (r1.running || r2.running) {
int count = DBManager.selectHostedGamesCount("afrixs");
System.out.println("count: "+count);
maxCount = Math.max(maxCount, count);
}
t1.join();
t2.join();
int gamesCount = DBManager.deleteHostedGames("afrixs");
System.out.println("Async test: count: "+gamesCount+", maxCount: "+maxCount+", ok1: "+r1.ok+", ok2: "+r2.ok);
assertTrue(maxCount <= 1 && gamesCount <= 1);
assertTrue(!(r1.ok && r2.ok));
}
}
private class CreateOneGameRunnable implements Runnable {
public boolean ok;
public boolean running = true;
private int number;
CreateOneGameRunnable(int number) {
this.number = number;
}
#Override
public void run() {
System.out.println("Starting "+number);
ok = DBManager.createOneGame("afrixs");
System.out.println("Finished "+number);
running = false;
}
}
}
First I tried out #Guillaume's suggestion to use user.getGames().add(game); along with game.setUser(user); when assigning the relation. gamesCountConstraintWorking test was successful, however, gamesCountConstraintWorkingAsync wasn't. It means that this attitude was successful in maintaining the session consistency (at the cost of fetching all user games), however, the database integrity wasn't maintained.
A solution that actually worked for both tests was (as #OrangeDog suggested) to add the constraint directly into database schema. MySQL:
DELIMITER $$
CREATE TRIGGER check_user_games_count
AFTER INSERT
ON Games FOR EACH ROW
BEGIN
DECLARE gamesCount INT;
SET gamesCount = (SELECT COUNT(id) FROM Games WHERE user_id = new.user_id);
IF gamesCount > 1 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'User may host at most 1 game';
END IF;
END $$
DELIMITER ;
So my summary is that Hibernate works great as a layer above the database to work with, but if you want to make sure the persisted data look like you want, you need to dive directly into your database schema and perform actions in there. (But that's only the result of this experiment, maybe someone knows a solution for this using Hibernate)
Note: I tried the tests with BEFORE UPDATE triggers and random delays inside the triggers and the tests were successful as well, it seems like some kind of lock is acquired for the table while inserting, so yes, this is a safe solution. (Note2: BEFORE UPDATE trigger for this needs gamesCount+1 > 1 condition and the constraint could fail (not tested) in the case of inserting multiple rows in one query)
Related
I try to define immutable Event-Entites (Event Sourcing) using Java JPA/Hibernate and want those events to have an absolute ordering that already is defined right after object-creation, before any persistence has taken place (no distributed setup where one would need consensus)
We are using automatic auditing properties using #CreatedDate but I crossed that off my list since this is only populated during persistence.
I can think of 2 options:
using a global database-sequence that is queried during object-creation
Long ordering = System.nanoTime()
any advice/idea appreciated.
want those events to have an absolute ordering that already is defined
right after object-creation
The simpliest solution is like:
public class SomeEntity {
private LocalDateTime createdAt = LocalDateTime.now();
}
Of course it is possible that two objects will be created simultaneously and will have the same date, but it might be extremely hard to get.
A bit more complex - ordered by id if dates are equal and both objects are persisted. There may be indeterminacy if some of objects is not persisted yet, but if both are persisted - strict order is guaranteed:
public class SomeEntity implements Comparable<SomeEntity> {
private Long id;
private LocalDateTime createdAt = LocalDateTime.now();
#Override
public int compareTo(SomeEntity o) {
int result = createdAt.compareTo(o.createdAt);
if (result == 0) {
if (id != null && o.id != null) {
result = id.compareTo(o.id);
} else if (id != null) {
result = 1;
} else if (o.id != null) {
result = -1;
}
}
return result;
}
}
The most complex option but strict ordering is guaranteed: you can create counter service in your JVM and create events through factory, that will use that counter during event creation.
public class CounterService {
AtomicInteger counter = new AtomicInteger();
public int getNext() {
return counter.incrementAndGet();
}
}
public class SomeEntityFactory {
private CounterService counterService;
public SomeEntity create() {
return new SomeEntity(counterService.getNext());
}
}
public class SomeEntity {
private int order;
SomeEntity(int order) {
this.order = order;
}
}
Of course, this is example only, counter service might return BigInteger and be a web service, for instance. Or you can use a database sequence like a counter.
All I have to do is get the return value from the insert as a long. I got that but it isn't working, I am getting back 0 from the returned value. I am using a DAO, Repository and ViewModel as stated in the Google CodeLabs. I have followed this post Rowid after Insert in Room.
Player Class
#Entity(tableName = "player_table")
public class Player {
#PrimaryKey(autoGenerate = true)
private long id;
#NonNull
#ColumnInfo(name = "username")
private String username;
}
DAO
#Insert
long insert(Player player);
Repository
public long insert(Player player) {
new insertAsyncTask(mPlayerDao).execute(player);
rowId = player.getId();
return rowId;
}
ViewModel
public long insert(Player player){
rowId = mRepository.insert(player);
return rowId;
}
Activity
String playerString = editTextUsername.getText().toString();
Player player = new Player(playerString);
long rowId = mDreamViewModel.insert(player);
The problem is that you return player.getId() before the AsyncTask finishes its background work of insertion; you must wait until it delivers you the correct result of insertion, and here is a suggested solution using the thread-safe CountDownLatch which pauses the the execution of the subsequent code using .await() method until count of the CountDownLatch reaches 0; where it decrements by 1 each time .countDown() is invoked.
public class Repository {
private long rowId = -1;
private CountDownLatch mLatch;
public long insert(Player player) {
mLatch = new CountDownLatch(1);
new insertAsyncTask(mPlayerDao).execute(player);
try {
mLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i("LOG_TAG", String.valueOf(rowId));
return rowId;
}
class insertAsyncTask extends AsyncTask<Player, Void, Void> {
#Override
protected Void doInBackground(Player... players) {
rowId = mDatabase.getContactDAO().addContact(players[0]);
mLatch.countDown();
return null;
}
}
}
The insert method in your Repository class can be changed to this:
public long insert(Player player) {
return new insertAsyncTask(mPlayerDao).execute(player).get();
}
I found this by searching GitHub for AsyncTasks with "Void, Long" as the last two types, along with some other Room-specific terms.
When debugging my own application I saw that execution of the return statement in the insert method was occurring before the doInBackground method was executed, which seemed obvious after I saw it happen.
I need something that seems not so specific but anyway I was unable to come up with nice and sophisticated solution.
Say I have very simple hibernate/jpa entity:
#Entity(name="entity")
public class Type {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#Column(unique = true, nullable = false)
private String name;
#Column(unique = false, nullable = false)
private boolean defaultType;
}
What i need is to somehow annotate defaultType field so only (and exactly) one persisted entity have this value as true. When new entity get persisted with this defaultType as true, the old one (with defaultType=true) entity has to be altered and its defaultType value changed to false. Also if any entity get changed (its defaultType got changed to true), same rule should apply.
As far I know this can be achieved inside business logic (e.g. in DAO layer), with DB trigger or with hibernates interceptor or event (If there is another way, please let me know). I tried with DAO solution but it's kind of bad solution because it can be bypassed and it is really clumsy for such simple operation. DB triggers can not be added with hibernate/jpa annotations (if I am not mistaken) and i am not sure how to make this functionality with hibernate interceptors/events.
So, what is best solution for this problem?
You need use Callback method in JPA, for example PreUpdate or PostUpdate, for instance:
#Entity
#EntityListeners(com.acme.AlertMonitor.class) // set callback method in another class
public class Account {
Long accountId;
Integer balance;
boolean preferred;
#Id
public Long getAccountId() { ... }
...
public Integer getBalance() { ... }
...
#Transient
public boolean isPreferred() { ... }
...
public void deposit(Integer amount) { ... }
public Integer withdraw(Integer amount) throws NSFException {... }
#PreUpdate // callback method in some class
protected void validateCreate() {
if (getBalance() < MIN_REQUIRED_BALANCE)
throw new AccountException("Insufficient balance to open an
account");
}
#PostUpdate // callback method in some class
protected void adjustPreferredStatus() {
preferred =
(getBalance() >= AccountManager.getPreferredStatusLevel());
}
}
// callback method in another class
public class AlertMonitor {
#PreUpdate // callback method in another class
public void updateAccountAlert(Account acct) {
Alerts.sendMarketingInfo(acct.getAccountId(), acct.getBalance());
}
}
Update: About your question, If I undestand what you want, this code may help you:
#Entity(name="entity")
#EntityListeners(com.yourpackage.TypeListner.class)
public class Type {
...
#Column(unique = false, nullable = false)
private boolean defaultType;
}
public class TypeListner {
pivate static Type objectWithTrue = null;
public void init() { // call this method when application is started
List<Type> results = entityManager
.createQuery("from Type", Type.class)
.getResultList();
for(Type type: results) {
if(type.getDefaultType()) {
objectWithTrue = type;
}
}
}
private void changeDefaultType(Type changed) {
if(changed.getDefaultType()) {
if(changed != objectWithTrue && objectWithTrue != null) {
objectWithTrue.setDefaultType(false);
}
objectWithTrue = changed;
}
}
#PostPresist
public void newType(Type changed) {
changeDefaultType(changed);
}
#PostUpdate
public void updateType(Type changed) {
changeDefaultType(changed);
}
#PreRemove
public void removeType(Type changed) {
if(changed.getDefaultType() && objectWithTrue == changed) {
objectWithTrue = null;
}
}
OR
You can use listner #PreUpdate and #PrePresist and every times overwrite all Type objects without store any variable (it isn't so good for perfomance then first example, but more reliable):
#PreUpdate
void updateType(Type changed) {
if(changed.getDefaultType()
List<Type> results = entityManager
.createQuery("from Type", Type.class)
.getResultList();
for(Type type: results) {
if(changed != type && type.getDefaultType()) {
type.setDefaultType(false);
}
}
}
}
I have a simple oneToMany relationship provided in Parent and corresponding ManyToOne in the Chile Entity class:
Parent:
#Entity
#Table(name = "FormExtraInfo")
#PrimaryKeyJoinColumn(name="form_container_id")
public class Form extends Container {
private List<Reason> reasons = new ArrayList<Reason>();
#OneToMany(mappedBy="form",cascade={javax.persistence.CascadeType.ALL},orphanRemoval=true)
#Cascade(value={CascadeType.ALL})
public List<Reason> getReasons() {
return reasons;
}
public void setReasons(List<Reason> reasons) {
this.reasons = reasons;
}
public void addReason(Reason reason) {
if (this.reasons == null) {
this.reasons = new ArrayList<Reason>();
}
this.reasons.add(reason);
}
}
Child class:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="Container_id")
public Form getForm() {
return form;
}
public void setForm(Form form) {
this.form = form;
}
Action class:
//Set the reasons
String[] reasonStatus = strutsForm.getMultiValueProperty(REASON_STATUS);
String[] reasonText = strutsForm.getMultiValueProperty(REASON_TEXT);
List<Reason> reasons = new ArrayList<Reason>();
logger.debug("form container ID : " + form.getId() +". # of Reasons for this form: "+ reasonText.length);
for (int i = 0; i < reasonText.length; i++) {
Reason r = new Reason();
r.setComment(reasonText[i]);
r.setStatusTypeCode(reasonStatus[i]);
r.setForm(form);
reasons.add(r);
}
form.setReasons(reasons);
Example case:
Status_code Reason_text
abc abc1
xyz xyz1
save the form:
Status_code Reason_text
abc abc1
xyz xyz1
abc abc1
xyz xyz1
With any operation : New insert or delete or update, it first duplicates the old data to the DB and then the operation that I performed.
Try replacing the cascade clause for this
#Cascade (value={CascadeType.SAVE_UPDATE,CascadeType.DELETE_ORPHAN})
Take a look at my blog post on mapping one-to-many http://arecordon.blogspot.com.ar/2013/05/hibernate-mapping-associations-one-to_20.html
If you can use a Set instead of a List; then, try changing the collection to a Set and make sure you overwrite equals() hashCode() as specified in here:
https://community.jboss.org/wiki/EqualsAndHashCode?_sscc=t
Also, remove the duplicated cascaded, you ca use:
#OneToMany(mappedBy="form",cascade={javax.persistence.CascadeType.ALL},orphanRemoval=true)
or
#OneToMany(mappedBy="form")
#Cascade(value={CascadeType.ALL, org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
I have a GUI where a list of teachers is shown. Two are selected by the user - they are going to be the form teachers of a new school class that gets created.
The teacher - class relationship is n-m.
School class: (it inherits its id from its group)
#Entity
#Table(name="school_classes")
#Cacheable(true)
public class SchoolClass extends Group{
#ManyToMany(cascade=CascadeType.ALL, mappedBy="classes", fetch=FetchType.EAGER)
private SortedSet<Teacher> teachers;
Teacher:
#Entity
#Table(name="teachers")
#Cacheable(true)
public class Teacher extends creator.models.school.Entity{
// name etc
#ManyToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinTable(name="class_teachers",
joinColumns = #JoinColumn(name="teacher_id", referencedColumnName="id"),
inverseJoinColumns = #JoinColumn(name="class_id", referencedColumnName="id"))
private SortedSet<SchoolClass> classes;
I try to create a new class like this:
String className = request.getParameter("class_name");
String id1 = request.getParameter("class_teacher1");
String id2 = request.getParameter("class_teacher2");
Teacher t1 = DataRetrieveModule.getTeacher(id1);
Teacher t2 = DataRetrieveModule.getTeacher(id2);
Layout l = new Layout();
SchoolClass newClass = new SchoolClass(className);
newClass.setLayout(l);
newClass.addTeacher(t1);
t1.addClass(newClass);
newClass.addTeacher(t2);
t2.addClass(newClass);
DataInsertionModule.insert(newClass);
This statement DataRetrieveModule.getTeacher(id1) opens a session, retrives the teacher by ID and closes it.
DataInsertionModule.insert(newClass) also opens a session, calls session.saveOrUpdate(newClass). (I also tried session.merge(newClass))
Retrieving a teacher:
public static Teacher getTeacher(String id) {
Session session = null;
Teacher t = null;
try{
sessionFactory = MyFactory.getSessionFactory();
session = sessionFactory.openSession();
t = (Teacher) session.get(Teacher.class, Long.parseLong(id));
}
catch(Exception e) {
System.out.println("in DAO:");
e.printStackTrace();
if(session!=null)
session.close();
}
finally{
if(session!=null)
session.close();
}
return t;
}
Data insertion:
public static void insert(Object o) {
Session session = null;
try
{
sessionFactory = MyFactory.getSessionFactory();
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(o);
tx.commit();
System.out.println("insertion done");
}
catch (Exception e)
{
System.out.println(e.getMessage());
e.printStackTrace();
}
finally
{
if(session!=null)
session.close();
}
}
But insertion never works.
There is always an object with the same id that already exists.
a different object with the same identifier value was already associated with the session: [creator.models.school.Teacher#3]
I searched on stackoverflow and have overridden my getHashCode and equals method in the class all my business objects inherit from:
#Override
public int compareTo(Entity o) {
if(this.id < o.getId())
return -1;
else if(this.id > o.getId())
return 1;
return this.getClass().getName().compareTo(o.getClass().getName());
}
#Override
public boolean equals(Object o) {
if(o instanceof Entity)
return this.compareTo((Entity)o)==0;
else return this.equals(o);
}
#Override
public int hashCode() {
char[] bla = this.getClass().getName().toCharArray();
int blue=0;
for(char c:bla)
blue = blue*10 + c;
return (int) (id+blue);
}
Furthermore I tried to change the CascadeType of ManyToMany to MERGE (reverted again).
At the moment I merge the teacher objects after retrieving both of them with session.get(..), as retrieving takes a lot of data out of the DB. There are futher ..ToMany relations. Therefore it probably might happen, that the call causes that both teachers are loaded.
public static Object merge(Object o) {
Session session = null;
Object returnVal = null;
try
{
sessionFactory = MyFactory.getSessionFactory();
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
returnVal = session.merge(o);
tx.commit();
}
catch (Exception e)
{
System.out.println(e.getMessage());
e.printStackTrace();
}
finally
{
if(session!=null)
session.close();
}
return returnVal;
}
.
Teacher t1 = DataRetrieveModule.getTeacher(id1);
Teacher t2 = DataRetrieveModule.getTeacher(id2);
t1= DataInsertionModule.merge(t1);
t2= DataInsertionModule.merge(t2);
Therefore I thought, that if I merge the one my get-method returned with the one that must have been loaded by the get call for the other teacher, it should work. (like here: http://www.stevideter.com/2008/12/07/saveorupdate-versus-merge-in-hibernate/) But it does not :(
Is it probably because an object of the same superclass (Object or my Entity class) has the same ID?
Please help me!
One potential situation that could lead to this error is when the id of teacher 1 and teacher 2 is the same. Since you close the session between each get they will be detached, and you would end up loading two different objects that both represent the same row in the database (they have the same primary key.) Then, when these are both going to be reattached to a session through a SchoolClass being saved, Hibernate will see two different objects both representing the same row in the database, and not know which one represent the correct state to persist to the database.
Don't know for sure if this is the case here, but you should run a debugger and check the state of the objects referenced by teacher1 and teacher2.