JPA/Hibernate批量插入数据

27

在阅读了关于jpa批量插入的几个话题后,我创建了一个简单的示例。 我有两个持久化对象User和Site。 一个用户可以拥有多个站点,因此我们在这里有一对多的关系。 假设我想创建一个用户并将几个站点链接到用户帐户。 这是代码的样子,考虑到我想要使用Site对象的批量插入。

User user = new User("John Doe");

user.getSites().add(new Site("google.com", user));
user.getSites().add(new Site("yahoo.com", user));

EntityTransaction tx = entityManager.getTransaction();
tx.begin();
entityManager.persist(user);
tx.commit();

但是当我运行这段代码时(我使用Hibernate作为JPA实现提供者),我看到以下SQL输出:

Hibernate: insert into User (id, name) values (null, ?)
Hibernate: call identity()
Hibernate: insert into Site (id, url, user_id) values (null, ?, ?)
Hibernate: call identity()
Hibernate: insert into Site (id, url, user_id) values (null, ?, ?)
Hibernate: call identity()

那么,我的意思是“真正的”批量插入不起作用,还是我被搞糊涂了?

这里是示例项目的源代码,这是一个Maven项目,所以只需下载并运行mvn install即可检查输出。

更新:

在Ken Liu的友情建议下,我已禁用了Site对象ID自动生成:

    User user = new User("John Doe");
    user.getSites().add(new Site(1, "google.com", user));
    user.getSites().add(new Site(2, "yahoo.com", user));
    entityManager.setFlushMode(FlushModeType.COMMIT);
    EntityTransaction tx = entityManager.getTransaction();
    tx.begin();
    entityManager.persist(user);
    tx.commit();

现在我在调试输出中看到以下内容:

DEBUG: org.hibernate.jdbc.AbstractBatcher - 执行批量大小:2

它可以工作!

3个回答

22
如果您使用数据库生成 ID,那么 Hibernate 将为每个实体执行查询以生成主键。

5
现在你是如何生成你的密钥的?你必须确保你的密钥是唯一的。 - Ken Liu
2
能否让 Hibernate 在插入 1000 个对象之前执行 UPDATE sometbl SET counter=counter+1000,然后只使用它刚刚保留的 1000 个 ID? - aioobe
1
@KenLiu 这是否意味着在使用Oracle时,当主键通过序列生成时,您无法批量插入多个条目? - bish

8
我发现在批量插入时,绕过Hibernate会更加高效。您必须放弃ORM(对象关系映射),但仍可以利用与当前会话和事务管理相关联的连接。
虽然您暂时失去了ORM的便利性,但回报是显着的,特别是如果您拥有本地生成的Ids,因为Hibernate通常会为每个INSERT执行一个SELECT。
Session.doWork非常方便,可促进此操作。
private MyParentObject saveMyParentObject(final MyParentObject parent, final List<MyChildObject> children)
{
    transaction = session.beginTransaction();
    try
    {
        session.save(parent); // NOTE: parent.parentId assigned and returned here

        session.doWork(new Work()
        {
            public void execute(Connection con) throws SQLException
            {
                // hand written insert SQL - can't use hibernate
                PreparedStatement st = con.prepareStatement("INSERT INTO my_child (parent_id, name, ...) values (?, ?, ...)");

                for (MyChildObject child : children)
                {
                    MyChildObject child = new MyChildObject();
                    child.setParentId(parent.getParentId()); // assign parent id for foreign key

                    // hibernate can't help, determine jdbc parameters manually
                    st.setLong(1, child.getParentId());
                    st.setString(2, child.getName());
                    ...
                    st.addBatch();
                }

                // NOTE: you may want to limit the size of the batch
                st.executeBatch();
            }
        });

        // if your parent has a OneToMany relationship with child(s), refresh will populate this 
        session.refresh(parent);
        transaction.commit();
        return parent;
    }
    catch(Throwable e)
    {
        transaction.rollback();
        throw new RuntimeException(e);
    }   
}

1
我正在使用您提供的相同技术。但仍然存在一个问题:该方法为每个插入准备了不同的语句,如 SQL Profiler 中所示。为了提高性能,需要编译或准备一次语句,然后调用该编译过的语句进行其余的插入操作。 - rogue lad
从数据库管理系统的角度来看,该语句是相同的,并且将选择相同的访问计划。只有在未使用变量绑定时(不是这种情况)才会有所不同。 - Lluis Martinez
保存大约50k条记录只需要大约2秒钟!太棒了,谢谢! - Ran

5

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