阻止Hibernate更新现有记录

4
我在尝试让Hibernate每次执行保存操作时都创建新的对象,而不是更新现有的记录。
public class Risk {
    private long riskID;
    private Vehicle vehicle;
}

public class Vehicle {  
    private long vehicleID;
    private long riskID;
    private String regNumber;
    private String abiCode;
    private String make;
    private String model;
}

如果我将风险写入数据库,然后在网页上更改车辆并尝试第二次保存风险到数据库。我希望在风险表中有两个风险,在车辆表中有两个车辆。

目前我正在使用Hibernate Session Save(Object o)。这总是在数据库中创建一个新的风险,但从未创建一个新的车辆。它只是更新原始的车辆。

以下是我的映射文件:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class lazy="false" name="uk.co.test.Risk" schema="quote_engine" table="GV_RISK" >
    <id column="riskID" name="riskID" type="long">
        <generator class="identity"/>
    </id>

    <many-to-one name="vehicle" class="uk.co.test.Vehicle" column="vehicleID" not-null="true" cascade="all" unique="true" />

</class>

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="uk.co.test.Vehicle" schema="quote_engine" table="GV_VEHICLE">

    <id name="vehicleID" type="long" column="vehicleID">
        <generator class="identity" />
    </id>

    <property name="regNumber" type="string" column="regNumber" />
    <property name="abiCode" type="string" column="abiCode" />
    <property name="make" type="string" column="make" />
    <property name="model" type="string" column="model" />


</class>


你如何更改车辆信息?Vehicle的映射文件在哪里? - Perception
我会在Java中进行更改。 - shawsy
你的代码是如何保存风险和车辆信息的? - Ken Chan
我觉得你的需求比你所表达的要广泛一些,如果我理解错了,请纠正我,但我认为你正在寻找某种审计功能。如果我是对的,你应该检查一下Hibernate Envers。 - Claude Vedovini
那么谁应该获得赏金呢? - Mark W
12个回答

6
我非常怀疑让Hibernate自动执行此操作是否可行。您想覆盖ORM中最基本的行为之一。它们的整个作用是在某些会话中跟踪您的对象,以便在不必手动排序更新的情况下将这些更新持久化到数据库中。
如果您不希望记录被更新,请将setter设置为私有,并注释字段,以便获得字段访问权限。在xml中,在标记上设置default-access="field"属性。
然后,在将对象添加到会话之前,您需要创建一个新对象。创建一个类似以下方法的服务:
public void saveVehicle(Vehicle vehicle) {
  Vehicle vehicleNew = vehicle.copy();
  // Risk would be copied via risk.copy() in vehicle.copy()

  vehicleDao.save(vehicleNew)
}

1
这种方法是可行的,但我建议重新表述关于“...他们在Session中跟踪您的对象...”的陈述。Session只是一个抽象层次:最终Hibernate是将Java对象与数据库行绑定,在任意数量的Sessions之间进行。这就是为什么像其他人建议的Session.evict()不起作用的原因。 - sharakan
小细节:vehicle.copy() 更符合面向对象的编程思想,风险也是一样。 - ewernli
同意,我改变了我的解决方案。 - Tim Pote

4
我认为更简单的方法是:
  1. 在一个Session中保存风险和车辆。
  2. 在关闭Session后,将ID设置为空。
  3. 需要时,重新保存对象。
很重要的是要关闭Hibernate Session(针对每次保存使用单独的Session),这样对象就会变得分离,当对象重新附加时,Hibernate会认为它们是全新的。
注意:应将long更改为Long(基本类型转换为包装类型)。

这是我目前的做法。只是觉得可能有更简洁的方式。 - shawsy

3

可能两个风险实例在它们的多对一关系中具有相同的车辆实例。如果您修改此车辆实例,则会同时修改两个风险实例的该实例,而不是创建一个新实例。

您必须将车辆实例清除或保存克隆实例。

我提供两种可能性的思路(非工作代码)。假设风险实例为r1和r2。

1)使用evict:

Risk r2 = r1.clone(); // or whatever you do to create your second risk instance

session.evict(r2.getVehicle());
session.save(r2.getVehicle()); // session.save(r2) also should work.

注意:在这种情况下,r1也指的是r2的车辆实例。如果您不再使用r1实例,则它将正常工作。如果仍然想要使用r1实例,则需要将其逐出,并重新从数据库中加载它。
2)使用克隆方法:
Risk r2 = r1.clone(); // or whatever you do to create your second risk instance

Vehicle v2 = r1.getVehicle.clone(); // create a new instance!
r2.setVehicle(v2);
session.save(r2);

成功与否取决于您clone()的实现方式。如果它默认复制ID,则无法正常工作,因为您将具有指向相同持久化逻辑车辆的不同Java实例车辆。 - sharakan
@sharakan:可能是这样。代码只需要表达思路,clone()不是重点。重点是要么驱逐车辆,要么创建一个新的车辆实例。 - Johanna
实际上,在1)中,clone()必须按照我描述的方式工作,否则它就是相同的逻辑对象,保存将更新相同的数据库行。Session.evict()不会使对象再次成为瞬态,只是从Session级别缓存中删除它。在2)中,这将取决于所使用的会话是否已将该车辆实例与其关联。 - sharakan

2

我假设一辆车可以有许多风险,是吗?

风险等级和风险的映射看起来没问题,但车辆呢?

类应该是:

public class Vehicle
{  
    private long vehicleID;
    private Set<Risk> risks;
    private String regNumber;
    private String abiCode;
    private String make;
    private String model;
}

请查看风险集(来自于许多风险与一个车辆的关系)。此外,在您的车辆映射中,您应该有类似以下内容:

<set name="risks" table="GV_RISK">
    <key column="riskID" />            
    <one-to-many class="Risk"/>
</set> 

希望这能帮到你,祝你好运 :)

1
不,这是一对一映射。这就是为什么我使用了多对一,但带有unique=true属性。从阅读许多示例来看,这就是如何映射一对一的方式。 - shawsy

2

查看每个表的DDL会有所帮助。在使用唯一外键关联时,应该使用“多对一”映射来表示“一对一”的关系。如果它们共享相同的主键,您可以使用“一对一”。

您可以尝试在车辆映射中进行双向映射,但这并不是必需的。这是它的样子。

<one-to-one name="risk" class="uk.co.test.Risk" property-ref="riskID"/> 

然而,你所尝试的事情基本上是正确的。我可能会稍微更改类定义并仔细检查你的DDL。
例如(保持hbm映射与上面定义的完全相同):
表结构如下:
create table GV_VEHICLE (
    vehicleID int(11) not null auto_increment,
    regNumber varchar(50) not null,
    abiCode varchar(50) not null,
    make varchar(50) not null,
    model varchar(50) not null,
    primary key(vehicleID)));

create table GV_RISK (
    riskID int(11) not null auto_increment,
    vehicleID int(11),
    primary key(riskID),
    foreign key(vehicleID) references GV_VEHICLE(vehicleID));

创建一对一的类定义,每个类都要引用彼此。
public class Vehicle {  
    private long vehicleID;
    private String regNumber;
    private String abiCode;
    private String make;
    private String model;
    private Risk risk;
}

public class Risk {
    private long riskID;
    private Vehicle vehicle;
}

2
我会假设在你的情况下执行的代码类似于以下内容:
vehicle = new Vehicle(make, model, regNumber, abiCode)
risk = new Risk(vehicle);
session.save(risk);

vehicle.setMake(otherMake)
risk = new Risk(vehicle)
session.save(risk);

你遇到的问题是,一旦调用第一个session.save(),Hibernate中的Risk和Vehicle对象就会从“瞬态”转变为“持久态”。这些状态在Hibernate ORM手册中有详细介绍。
通过在已持久化的车辆上设置制造商,你明确表示想要编辑数据库中现有的行,而不是创建新行。如果想要创建新行,请参考Hibernate手册中的信息,并将第二个段落更改为:
Vehicle vehicle2 = new Vehicle(otherMake, model, regNumber, abiCode)
risk = new Risk(vehicle2);
session.save(risk);

2

其中一种可能的解决方案是使用本地SQL覆盖Vehicle的sql-update。

这里有一个人的示例:

<class name="Person">
<id name="id">
    <generator class="increment"/>
</id>
<property name="name" not-null="true"/>
<sql-insert>INSERT INTO PERSON (NAME, ID) VALUES ( UPPER(?), ? )</sql-insert>
<sql-update>UPDATE PERSON SET NAME=UPPER(?) WHERE ID=?</sql-update>
<sql-delete>DELETE FROM PERSON WHERE ID=?</sql-delete>
</class>

所以,您可以仅重写sql-update以执行插入操作吗?

2

我认为你需要看一下Hibernate Envers (http://docs.jboss.org/hibernate/orm/4.1/devguide/en-US/html/ch15.html)


1
使用Session.evict。在你驱逐一个对象之后,它不会在数据库中更新:
Entity entity = ....
session.evict(entity);
entity.setA(a); // won't affect the database

因此,除非您明确调用“update”、“merge”(或“saveOrUpdate”),否则对象不会在数据库中更新。


Session.evict 旨在从 Session 级别缓存中移除对象。它不会取消关联 Java 对象与数据库级别信息(即,它处于“分离”状态,尚未变为瞬态)。 - sharakan
但这意味着对象在数据库中不会被更新,除非您明确调用“update”或“merge”(或“saveOrUpdate”)。 - Eugene Retunsky

1
Hibernate是使用监听器构建的,Hibernate生态系统中的每个活动都有相应的监听器。 DefaultSaveOrUpdateEventListener用于使用Hibernate保存或更新对象, onSaveOrUpdate(org.hibernate.event.SaveOrUpdateEvent)方法将包含正在保存的对象的信息。 当您调用session.save(modelObject)时,您的调用将最终进入监听器,在此处您可以将ID设置为null,系统将每次添加新记录。

这里是一个示例 这篇博客文章详细介绍了监听器的使用

<property name="eventListeners"> <map> <entry key="save-update"><ref local="saveUpdateListener" /></entry> </map> </property>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接