多表批量操作

作者:    |      

正如我在之前关于 批量操作 的博客中提到的,对于跨越多个表的单一实体(不包括关联),UPDATE 和 DELETE 语句都是一项挑战,这可能是使用

  • 继承<joined-subclass/>
  • 继承<union-subclass/>
  • 实体映射<join/>结构

为了说明,让我们使用以下继承层次结构

Animal
  /   \
 /     \
Mammal   Reptile
   / \
  /   \
Human   Dog

所有这些都使用连接子类策略进行映射。

删除

删除有三个相关挑战。

  • 针对多表实体的删除需要递归级联到
  • 所有匹配主键(PK)值的子类行
  • 其超类行
  • 所有这些协调的删除都需要按顺序发生,以避免约束违规
  • 需要删除哪些行?

考虑以下代码

session.createQuery( "delete Mammal m where m.age > 150" ).executeUpdate();

很明显,我们需要从 MAMMAL 表中删除。此外,MAMMAL 表中的每一行都在 ANIMAL 表中都有一个对应的行;因此,对于从 MAMMAL 表中删除的任何行,我们都需要删除对应的 ANIMAL 表行。这满足了向超类级联。如果 Animal 实体本身有超类,我们还需要删除那一行,等等。

接下来,MAMMAL 表中的行可能在 HUMAN 表或 DOG 表中都有对应的行;因此,对于从 MAMMAL 表中删除的每一行,我们都需要确保从 HUMAN 或 DOG 表中删除任何对应的行。这满足了向子类级联。如果 Human 或 Dog 实体有更深的子类,我们还需要删除那些行,等等。

我之前提到的一个挑战是正确排序删除操作,以避免违反任何约束。在我们示例结构中,典型的外键(FK)设置是将FK指向层次结构的上方。因此,MAMMAL表从其主键(PK)到ANIMAL表的主键有外键,等等。所以我们需要确保删除的顺序。

( HUMAN | DOG ) -> MAMMAL -> ANIMAL

在这里,无论是先删除HUMAN表还是先删除DOG表,实际上并没有太大关系。

那么,哪些行需要被删除(这个问题也适用于更新语句)?大多数数据库不支持联合删除,所以我们确实需要针对涉及的每个单独表分别执行删除操作。一种简单的方法是使用子查询返回受限制的PK值,并将用户定义的限制作为删除语句的限制。这在之前的示例中是可行的。但是,考虑另一个示例

session.createQuery( "delete Human h where h.firstName = 'Steve'" ).executeUpdate();

我之前说过,我们需要按顺序删除,以避免违反定义的外键约束。在这里,这意味着我们需要先从HUMAN表删除;因此,我们可以执行一些类似以下SQL的语句

delete from HUMAN where ID IN (select ID from HUMAN where f_name = 'Steve')

到目前为止,一切顺利;也许这不是最有效的方法,但它可行。接下来,我们需要从MAMMAL表中删除相应的行;因此,我们可以执行一些更多的SQL语句

delete from MAMMAL where ID IN (select ID from HUMAN where f_name = 'Steve')

哎呀!这不会工作,因为我们之前已经从HUMAN表中删除了任何这样的行。

那么我们如何解决这个问题呢?我们肯定需要预先选择并存储匹配给定WHERE子句限制的PK值。一种方法是通过JDBC选择PK值并将它们存储在JVM内存空间内;然后稍后,将这些PK值绑定到各个删除语句中。类似于以下内容

PreparedStatement ps = connection.prepareStatement( 
        "select ID from HUMAN where f_name = 'Steve'"
);
ResultSet rs = ps.executeQuery();
HashSet ids = extractIds( rs );
int idCount = ids.size();

rs.close();
ps.close();

....

// issue the delete from HUMAN
String sql = 

ps = connection.prepareStatement(
        "delete from HUMAN where ID IN (" +
        generateCommaSeperatedParameterHolders( idCount ) +
        ")"
);
bindParameters( ps, ids );
ps.executeUpdate();

...

另一种方法,Hibernate采用的方法,是利用临时表;在数据库服务器上存储匹配的PK值。这在许多方面都更有性能,这也是选择这种方法的主要原因。现在我们有以下内容

// where HT_HUMAN is the temporary table (varies by DB)
PreparedStatement ps = connection.prepareStatement( 
        "insert into HT_HUMAN (ID) select ID from HUMAN where f_name = 'Steve'"
);
int idCount = ps.executeUpdate();
ps.close();

....

// issue the delete from HUMAN 
ps = connection.prepareStatement(
        "delete from HUMAN where ID IN (select ID from HT_HUMAN)"
);
ps.executeUpdate();

在第一步中,我们避免了与返回结果相关的潜在网络通信开销;我们还避免了JDBC的一些开销;我们还避免了存储id值所需的内存开销。在第二步中,我们再次最小化了在我们和数据库服务器之间传输的数据量;驱动程序和服务器也可以将其识别为可重复的预编译语句并避免创建执行计划的开销。

更新

多表更新语句真正只有两个挑战

  • 从SET子句中分区赋值
  • 哪些行需要被更新?这个问题已经在上面讨论过了...

考虑以下代码

session.createQuery( "update Mammal m set m.firstName = 'Steve', m.age = 20" )
        .executeUpdate();

从前面的例子中我们可以看到,age属性实际上是在Animal超类中定义的,因此映射到ANIMAL.AGE列;而firstName属性是在Mammal类中定义的,因此映射到MAMMAL.F_NAME列。所以在这里,我们知道我们需要对ANIMAL和MAMMAL表执行更新(即使Mammal可能进一步是Human或Dog,也不会触及其他表)。分区赋值实际上就是识别哪些表受个别赋值的影响,然后构建适当的更新语句。这里的挑战在于在绑定用户提供的参数时考虑到这一点。尽管如此,分区赋值和参数基本上是一个学术练习。


返回顶部