Hibernate不一致地生成重复的主键

时间:2018-05-04 11:16:35

标签: java spring hibernate

我正在使用Spring和Hibernate(hibernate-core 3.3.1.GA),并且作为Web调用的结果,代码执行具有多个插入的事务。有时,其中一个插入失败,Hibernate说'Duplicate entry ... for key 'PRIMARY'。当发生这种情况时,我无法确定任何模式 - 它可能适用于4-5个请求,然后失败,然后重试,然后在下一个请求时失败。

以下是代码的相关部分:

控制器

@RequestMapping(value = "/users", method = RequestMethod.POST)
public @ResponseBody Map<Object, Object> save(<params>) throws IllegalArgumentException {
    ...
    try {
            map = userHelper.save(<parameters>);
    ...
    } catch (Exception e) {
        e.printStackTrace();
    }
}

上述部分抛出异常。

UserHelper.save()方法

@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public HashMap<String, Object> save(<parameters>) throws NumberParseException, IllegalArgumentException, HibernateException {
    ....
    userService.save(<parameters>);
    return save;
}

UserService

HBDao dao;

@Autowired
public UserService(org.hibernate.SessionFactory sessionFactory) {
    dao = new HBDao(sessionFactory);
}
...
@Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
public HashMap<String, Object> save(<parameters>) throws NumberParseException {
    ...
    User user;
    // several lines to create User object
    dao.save(user);
    ...
    lookupService.saveUserConfigurations(user, userType, loginById);
    ...
    return response;
}

HBDao

这个类包装了hibernate会话。

public HBDao(SessionFactory sf) {
    this.sessionFactory = sf;
}

private Session getSession() {
    sessionFactory.getCurrentSession();
}

public void save(Object instance) {
    try {
        getSession().saveOrUpdate(instance);
    } catch (RuntimeException re) {
        throw re;
    }
}

lookupService.saveUserConfigurations(user, userType, loginById)调用会导致LookupRepository类中的以下方法执行:

LookupRepository

@Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
public LookupMapping save(LookupMapping configuration) {
    dao.save(configuration);
    return configuration;
}

public Collection<LookupMapping> saveAll(Collection<LookupMapping> configurations) {
    configurations.forEach(this::save);
    return configurations;
}

LookupMapping

@Entity
public class LookupMapping {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long configId;
    ...
}

LookupMapping类的Hibernate Mapping

<hibernate-mapping package="com...configuration.domain">
    <class name="LookupMapping" table="lookup_mapping" mutable="false">
        <id column="id" name="configId" type="long">
            <generator class="increment"/>
        </id>
        ...
    </class>
</hibernate-mapping>

Hibernate config

<hibernate-configuration>
    <session-factory name="sosFactory">
        <!-- Database connection settings -->
        ...

        <property name="connection.pool_size">2</property>

        <!-- SQL dialect -->
        <property name="dialect">com. ... .CustomDialect</property>

        <!-- Enable Hibernate's current session context -->
        <property name="current_session_context_class">org.hibernate.context.ManagedSessionContext</property>

        <!-- Disable the second-level cache -->
        <property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>

        <!-- Echo all executed SQL to stdout -->
        <property name="show_sql">true</property>
        <property name="format_sql">true</property>

        ...
</session-factory>
</hibernate-configuration>

以下是日志中的行:

2018-05-04 10:24:51.321 7|13|60f566fa-4f85-11e8-ba9b-93dd5bbf4a00 ERROR [http-nio-8080-exec-1] org.hibernate.util.JDBCExceptionReporter - Duplicate entry '340932' for key 'PRIMARY'
2018-05-04 10:24:51.321 7|13|60f566fa-4f85-11e8-ba9b-93dd5bbf4a00 WARN [http-nio-8080-exec-1] org.hibernate.util.JDBCExceptionReporter - SQL Error: 1062, SQLState: 23000
2018-05-04 10:24:51.322 7|13|60f566fa-4f85-11e8-ba9b-93dd5bbf4a00 ERROR [http-nio-8080-exec-1] org.hibernate.event.def.AbstractFlushingEventListener - Could not synchronize database state with session
org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update
    at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:94) ~[hibernate-core-3.3.1.GA.jar:3.3.1.GA]
    at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66) ~[hibernate-core-3.3.1.GA.jar:3.3.1.GA]
    at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:275) ~[hibernate-core-3.3.1.GA.jar:3.3.1.GA]
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:266) ~[hibernate-core-3.3.1.GA.jar:3.3.1.GA]
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:167) ~[hibernate-core-3.3.1.GA.jar:3.3.1.GA]
    at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321) [hibernate-core-3.3.1.GA.jar:3.3.1.GA]
    at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:50) [hibernate-core-3.3.1.GA.jar:3.3.1.GA]
    at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1027) [hibernate-core-3.3.1.GA.jar:3.3.1.GA]
    at com.arl.mg.helpers.UserHelper.save(UserHelper.java:329) [classes/:?]
...

我正在研究遗留代码库(因此无法轻松升级Hibernate),我编写的代码位于LookupRepository类(以及LookupService中调用UserService

持久化Duplicate entry个对象时发生LookupMapping错误。始终有两个此对象被持久化,并且在发生错误时,将创建与最后一个条目相同的重复ID。也就是说,如果第一个请求插入了ID 9991000,并且如果下一个请求发生错误,则重复的ID将为1000(而不是999)。

另一个可能需要注意的重要事项是Hibernate显示了这一行:

org.hibernate.jdbc.ConnectionManager [] - transaction completed on session with on_close connection release mode; be sure to close the session to release JDBC resources!

这是我到目前为止所有的信息,我希望我已经涵盖了相关的代码。任何帮助都感激不尽。如果我需要提供更多信息,请告诉我。

谢谢!

2 个答案:

答案 0 :(得分:5)

问题在于Hibernate映射文件中定义的ID生成策略。

策略设置为increment,这似乎仅在没有其他进程插入表时才有效。在我的情况下,似乎有时会有先前打开的会话,并且新请求最终同时插入到表中。

解决方案是将策略更改为native,它使用底层数据库的策略生成ID。

<hibernate-mapping package="com...configuration.domain">
    <class name="LookupMapping" table="lookup_mapping" mutable="false">
        <id column="id" name="configId" type="long">
            <generator class="native"/>
        </id>
        ...
    </class>
</hibernate-mapping>

答案 1 :(得分:3)

我同意@shyam的回复我会切换到一些序列生成器。

但也看看这段代码的和平:

User user;
// several lines to create User object
dao.save(user);
...
lookupService.saveUserConfigurations(user, userType, loginById);

在这种情况下,您将user发送给未经管理的saveUserConfigurations,并且saveUserConfigurations内您可能会调用merge方法。这将导致额外的insert语句。考虑将代码重构为:

User user;
// several lines to create User object
// dao.save should return the stored value of user.
user = dao.save(user);
...
lookupService.saveUserConfigurations(user, userType, loginById);

使用这样的结构,您将使用存储的实体(即由当前的hibernate会话管理)。并查看所有代码,并防止在存储后使用非托管实体。