Hibernate验证器对我来说可以很好地验证Hibernate获取的对象,但问题是我想确保在数据库中持久化/更新对象后满足某些条件。例如:
我的条件是:用户最多可以主持3个游戏
约束注释:
@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 { };
}
验证器:
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;
}
}
用户类别:
@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
}
游戏类别:
@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
}
测试方法:
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();
}
}
}
Test.hostGames("afrixs", 4)
的预期行为将失败。然而,验证器在更新之前验证用户对象的状态,即games.size((等于0,因此满足约束条件,并且在第二次调用Test.hostGames("afrixs", 4)
之前没有任何失败。当然,在这种情况下,我们可以手动将游戏添加到用户user.getGames().add(game)
,但这种态度很容易出错(游戏需要以这种方式添加到代码中的任何位置(,并且如果例如异步调用两个Test.hostGames("afrixs", 2)
,则无法解决问题。
所以我的问题是:使用hibernate约束数据库完整性的正确方法是什么?有没有一种方法可以让验证器在将对象存储到数据库后检查对象的最终状态?或者我需要手动执行约束(比如在session.getTransaction().commit
之后执行另一个事务,检查条件并在不满足条件的情况下回滚更新事务(?还是应该省略hibernate并使用SQL触发器?谢谢你的回答,它们将对有很大帮助
这是我当前的休眠验证配置:
<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>
好的,我做了一些实验,写了一个小的测试类。为了使事情变得简单,我将约束条件改为";用户最多可以主持1场游戏;。
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;
}
}
}
首先,我尝试了@Guillaume的建议,在分配关系时使用user.getGames().add(game);
和game.setUser(user);
。gamesCountConstraintWorking
试验成功,但gamesCountConstraintWorkingAsync
试验不成功。这意味着这种态度成功地维护了会话一致性(以获取所有用户游戏为代价(,然而,数据库的完整性没有得到维护。
实际上对这两个测试都有效的解决方案是(正如@OrangeDog所建议的(将约束直接添加到数据库模式中。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 ;
因此,我的总结是,Hibernate作为数据库之上的一层可以很好地工作,但如果你想确保持久化的数据看起来像你想要的,你需要直接进入你的数据库模式并在那里执行操作。(但这只是这个实验的结果,也许有人知道使用Hibernate的解决方案(
注意:我尝试了使用BEFORE UPDATE触发器和触发器内的随机延迟进行测试,测试也很成功,插入时似乎为表获取了某种锁,所以是的,这是一个安全的解决方案。(注意2:BEFORE UPDATE触发器需要gamesCount+1>1条件,在一个查询中插入多行的情况下,约束可能失败(未测试(