java - 使用 Hibernate 验证器验证数据库完整性
问题描述
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)
异步调用它并不能解决问题。
所以我的问题是:使用休眠约束数据库完整性的正确方法是什么?有没有办法让验证器在将对象存储到数据库后检查它们的最终状态?或者我是否需要手动进行约束(比如执行另一个事务session.getTransaction().commit
并检查条件,如果不满足则回滚更新事务)?或者我应该忽略休眠并为此使用 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;
}
}
}
首先,我在分配关系时user.getGames().add(game);
尝试了@Guillaume 的建议。测试成功了,但是,没有。这意味着这种态度成功地维护了会话一致性(以获取所有用户游戏为代价),但是,数据库完整性没有得到维护。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 条件,并且在一个查询中插入多行的情况下,约束可能会失败(未测试))
推荐阅读
- vuetify.js - v-combobox 无法列出项目
- javascript - 如何使用 Node.js 将文件写入 Postgres?
- javascript - 我如何使图像与下一个同时出现并在右边
- java - 在java中返回一个泛型而不强制转换它?
- python - python将列表中的项目解析为字符串
- python - 类型错误:'在
' 需要字符串作为左操作数,而不是列表错误 - javascript - 一个 PHP 程序中的 JS/HTML/PHP 和通过服务器发送事件的 MYSQL 更新
- switch-statement - Discord.js 在 Switch 中的默认命令问题
- android - 使用 Room 观察多个表中的数据变化
- regex - 用 1 个正则表达式匹配 2 个 Pulse Secure 事件