如何确保事务中的所有内容都可以看到事务中发生的数据库更改

时间:2010-10-25 08:43:07

标签: java hibernate transactions

我有一个事务可以保存或更新数据库中的一组对象。这是代码:

@Transactional
public void updatePDBEntry(Set<PDBEntry> pdbEntrySet) {
    for (PDBEntry pdbEntry : pdbEntrySet) {
        PDBEntry existingEntry = findByAccessionCode(pdbEntry.getAccessionCode());
        if (existingEntry != null) {
            log.debug("Remove previous version of PDBEntry {}", existingEntry);
            makeTransient(existingEntry);
        }
        makePersistent(pdbEntry);
    }
}

在genericDAO中:

public void makePersistent(I entity) {
    getCurrentSession().saveOrUpdate(entity);
}

public void makeTransient(I entity) {
    getCurrentSession().delete(entity);
}

不知怎的,这不起作用,它说因为重复的密钥而无法插入对象,即使我可以在日志中看到它获取makeTransient()。我想这与所有这一切都发生在事务中的事实有关,因此makePersistent()方法可能看不到makeTransient()所做的更改。我可以通过将所有数据从pdbEntry复制到existingEntry然后执行saveOrUpdate(existingEntry)来解决这个问题,但这是一种肮脏的黑客行为。有没有其他方法可以确保makeTransient对makePersistent可见,同时仍然将其全部保留在事务中?

编辑:这是我的PDBEntry域模型:

@Entity
@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(callSuper = false, of = { "accessionCode", "date" })
@SuppressWarnings("PMD.UnusedPrivateField")
public class PDBEntry extends DomainObject implements Serializable {
    @NaturalId
    @NotEmpty
    @Length(max = 4)
    private String          accessionCode;

    @NaturalId
    @NotNull
    @Temporal(TemporalType.DATE)
    private Date            date;

    private String          header;

    private Boolean         isValidDssp;

    @Temporal(TemporalType.TIMESTAMP)
    private Date            lastUpdated     = new Date(System.currentTimeMillis());

    @OneToOne(mappedBy = "pdbEntry", cascade = CascadeType.ALL)
    private ExpMethod       expMethod;

    @OneToMany(mappedBy = "pdbEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Refinement> refinementSet   = new HashSet<Refinement>();

    @OneToMany(mappedBy = "pdbEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<HetGroup>   hetGroupSet     = new HashSet<HetGroup>();

    @OneToMany(mappedBy = "pdbEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Chain>      chainSet        = new HashSet<Chain>();

    @OneToMany(mappedBy = "pdbEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Chain>      residueSet      = new HashSet<Chain>();

    public Date getLastUpdated() {
        return new Date(lastUpdated.getTime());
    }

    public void setLastUpdated() throws InvocationTargetException {
        throw new InvocationTargetException(new Throwable());
    }

    public void touch() {
        lastUpdated = new Date(System.currentTimeMillis());
    }

    @Override
    public String toString() {
        return accessionCode;
    }

    public PDBEntry(String accessionCode, Date date) throws NullPointerException {
        if (accessionCode != null && date != null) {
            this.accessionCode = accessionCode;
            this.date = date;
        } else {
            throw new NullPointerException();
        }
    }
}

@MappedSuperclass
public abstract class DomainObject implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long    id;

    public Long getId() {
        return id;
    }

    @Override
    public abstract boolean equals(Object obj);

    @Override
    public abstract int hashCode();

    @Override
    public abstract String toString();
}

编辑:新问题,我创建了一个方法,首先从数据库中删除所有现有对象,如下所示:

@Override
@Transactional
public void updatePDBEntries(Set<PDBEntry> pdbEntrySet) {
    findAndRemoveExistingPDBEntries(pdbEntrySet);
    savePDBEntries(pdbEntrySet);
}

@Override
@Transactional
public void findAndRemoveExistingPDBEntries(Set<PDBEntry> pdbEntrySet) {
    for (PDBEntry pdbEntry : pdbEntrySet) {
        PDBEntry existingPDBEntry = findByAccessionCode(pdbEntry.getAccessionCode());
        if (existingPDBEntry != null) {
            log.info("Delete: {}", pdbEntry);
            makeTransient(existingPDBEntry);
        }
    }
}

@Override
@Transactional
public void savePDBEntries(Set<PDBEntry> pdbEntrySet) {
    for (PDBEntry pdbEntry : pdbEntrySet) {
        log.info("Save: {}", pdbEntry);
        makePersistent(pdbEntry);
    }
}

它似乎删除了它遇到的前73个条目,但随后出现错误:

WARN  2010-10-25 14:28:49,406 main JDBCExceptionReporter:100 - SQL Error: 0, SQLState: 23503
ERROR 2010-10-25 14:28:49,406 main JDBCExceptionReporter:101 - Batch entry 0 /* delete nl.ru.cmbi.pdbeter.core.model.domain.PDBEntry */ delete from PDBEntry where id='74' was aborted.  Call getNextException to see the cause.
WARN  2010-10-25 14:28:49,406 main JDBCExceptionReporter:100 - SQL Error: 0, SQLState: 23503
ERROR 2010-10-25 14:28:49,406 main JDBCExceptionReporter:101 - ERROR: update or delete on table "pdbentry" violates foreign key constraint "fke03a2dc84d44e296" on table "hetgroup"
  Detail: Key (id)=(74) is still referenced from table "hetgroup".
ERROR 2010-10-25 14:28:49,408 main AbstractFlushingEventListener:324 - Could not synchronize database state with session
org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update

有任何想法可能会出现此错误吗?

编辑:我发现了问题:最后一个错误是由hetgroup模型引起的,如下所示:

@Entity
@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(callSuper = false, of = { "pdbEntry", "hetId" })
@SuppressWarnings("PMD.UnusedPrivateField")
// extends DomainObject which contains Id, NaturalId is not enough in this case, since duplicate chains still exist
// in fact this is an error of the PDBFinder and will be fixed in the future
public class HetGroup extends DomainObject implements Serializable {
    //@NaturalId 
    @NotNull
    @ManyToOne
    private PDBEntry    pdbEntry;

    //@NaturalId 
    @NotEmpty
    private String hetId;

    private  Integer nAtom;

    @Length(max = 8192)
    private String name;

    public HetGroup(PDBEntry pdbEntry, String hetId) {
        this.pdbEntry = pdbEntry;
        pdbEntry.getHetGroupSet().add(this);

        this.hetId = hetId;
    }
}

特别注意评论的@ NaturalId,我评论了这些因为我的数据中仍然有一些错误导致重复的hetgroups,所以我想我现在只删除它们的UNIQUE约束,但我忘了我我正在使用Lombok为我创建equals和hashcode方法,如下所示:

@EqualsAndHashCode(callSuper = false, of = { "pdbEntry", "hetId" })

这导致了重复HetId的错误。我修复了数据并恢复了@ NaturalId,现在一切正常。

全部谢谢!

2 个答案:

答案 0 :(得分:2)

单一交易不应该是问题。

您的建议 - 将数据从pdbEntry复制到existingEntry是一个更好的解决方案。它对数据库密集程度要低得多,而且更容易阅读和理解正在发生的事情。

此外,如果您这样做,则不需要对更改的对象执行saveOrUpdate。 Hibernate实现了所谓的“透明持久性”......这意味着hibernate负责确定需要采用哪些数据操作来使对象与数据库同步。 hibernate中的“更新”操作不适用于已经持久的对象。见here

在这种情况下,代码看起来像这样(注意不需要更新):

    public void updatePDBEntry(Set<PDBEntry> pdbEntrySet) {
        for (PDBEntry pdbEntry : pdbEntrySet) {
            PDBEntry existingEntry = findByAccessionCode(pdbEntry.getAccessionCode());
            if (existingEntry != null) {
                // copy relevant fields from pdbEntry to existing Entry - preferably with a method on PDBEntry
            } else {
                makePersistent(pdbEntry); // although better to just call _save_ (not saveOrUpdate) cause you know it's a new object
            }
        }            
    }

答案 1 :(得分:2)

  

不知怎的,这不起作用,它说由于重复的密钥,它无法插入对象,即使我可以在日志中看到它变为makeTransient()。

要了解此处发生的情况,首先需要了解Hibernate不会立即将更改写入数据库,更改会在会话中排队并在flush时间写入。因此,即使您看到makeTransient()被调用,这并不意味着在调用方法时实际上已从数据库中删除了相应的记录。当flush发生时(通过flush()明确调用commit(),或者org.hibernate.Transaction.commit()时执行,将执行SQL删除语句和其他待处理更改HQL查询)。这在文档中有很好的解释:

  

10.10. Flushing the Session

     

有时会话会执行   需要同步的SQL语句   JDBC连接的状态与   内存中保存的对象状态。这个   进程,称为 flush ,由   默认为以下几点:

     
      在某些查询执行之前
  •   
  • 来自Session.flush()
  •   
  • 来自session.save()
  •   
     

SQL语句发布于   以下顺序:

     
      
  1. 所有实体插入的顺序与对应的对象相同   使用Session.delete()
  2. 保存   
  3. 所有实体更新
  4.   
  5. 所有集合删除
  6.   
  7. 所有集合元素删除,更新和插入
  8.   
  9. 所有收集插入
  10.   
  11. 所有实体删除的顺序与相应的对象相同   已使用01: @Transactional 02: public void updatePDBEntry(Set<PDBEntry> pdbEntrySet) { 03: for (PDBEntry pdbEntry : pdbEntrySet) { 04: PDBEntry existingEntry = findByAccessionCode(pdbEntry.getAccessionCode()); 05: if (existingEntry != null) { 06: log.debug("Remove previous version of PDBEntry {}", existingEntry); 07: makeTransient(existingEntry); 08: } 09: makePersistent(pdbEntry); 10: } 11: }
  12. 删除         

    一个例外是对象使用   插入本机ID时生成   他们得救了。

         

    ...

所以,让我们再看一下你的代码:

session.delete()
  • 在04,您执行查询(这将刷新挂起的更改,如果有的话)
  • 在07,您执行session.save()(在记忆中)
  • 在09,你执行flush()(在记忆中)
  • 回到04,更新被刷新,Hibernate以上面指定的顺序发出SQL语句
    • 由于密钥违规导致插入失败,因为该记录尚未删除

换句话说,你的问题与交易无关,它只与Hibernate的工作方式和你使用它的方式有关。

  

是否有其他方法可以确保makeTransient对makePersistent可见,同时仍然将其全部保留在事务中?

好吧,在不改变逻辑的情况下,你可以在delete()之后明确地帮助Hibernate和{{1}}。

但IMO,整个方法都不是最理想的,你应该更新现有的记录,如果有的话,而不是删除然后插入(除非有充分的理由这样做)。

参考