前言: 之前的一个项目有个功能需要运行时创建/更新表结构,当时第一时间想到的是根据字段和表名拼接建表和更新表的sql,现在知道Hibernate有个动态模型(dynamic models)可以完成此功能,遂记录下。

Hibernate Dynamic models

官方文档
文档原话: 运行期的持久化实体没有必要一定表示为像POJO类或JavaBean对象那样的形式。Hibernate也支持动态模型 (在运行期使用Map的Map)和象DOM4J的树模型那 样的实体表示。使用这种方法,你不用写持久化类,只写映射文件就行了。

使用Hibernate的内置api来更新表

例子:

  1. 先定义一个映射文件student.hbm.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class entity-name="dyc_Student" table="dyc_student">
        <id name="id" type="java.lang.String"  >
            <generator class="uuid"></generator>
        </id>
        <property type="java.lang.String" name="username" column="username"/>
        <property name="password" type="java.lang.String" column="password"/>
        <property name="sex" type="java.lang.String"  length="300" column="sex"/>
        <property name="age" type="java.lang.Integer" column="age"/>
        <property name="birthday" type="java.util.Date" column="birthday"/>
    </class>
</hibernate-mapping>

跟一般的Hibernate实体映射文件有区别,hibernate-mapping 没有属性,class标签则必须声明entity-name。
2. 测试
pom配置:

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <springboot.version>2.4.5</springboot.version>
  </properties>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
      <version>${springboot.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <version>${springboot.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
      <version>${springboot.version}</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>RELEASE</version>
    </dependency>
@SpringBootTest(classes = {App.class})
public class SpringbootDbTest {

    @Autowired
    EntityManager entityManager;

    public static InputStream getXmlStream() {
        byte[] bytes = new byte[0];
        try (InputStream inputStream = App.class.getResourceAsStream("/student.hbm.xml")) {
            bytes = new byte[inputStream.available()];
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ByteArrayInputStream(bytes);
    }

    @Test
    public void testUpdateSchema(){
        SessionFactory sessionFactory = entityManager.getEntityManagerFactory().unwrap(SessionFactory.class);
        StandardServiceRegistry serviceRegistry = sessionFactory.getSessionFactoryOptions().getServiceRegistry();
        MetadataSources metadataSources = new MetadataSources(serviceRegistry);
        //读取映射文件
        metadataSources.addInputStream(getXmlStream());
        Metadata metadata = metadataSources.buildMetadata();

        //更新数据库Schema,如果不存在就创建表,存在就更新字段,不会影响已有数据
        SchemaUpdate schemaUpdate = new SchemaUpdate();
        schemaUpdate.execute(EnumSet.of(TargetType.DATABASE), metadata, serviceRegistry);
    }
}

一定要记得设置Hibernate方言配置。

后续

  1. 该方案的优点:
    可以定义个Hibernate模版文件,运行时用FreeMarker等模版引擎动态生成相应的映射文件流,这样比自己去写sql拼接优雅的多。而且还不用考虑数据库差异。
  2. 我看官方示例对动态模型能用hsql,试了下确实可以:
        Configuration cfg = new Configuration();
        cfg.addInputStream(getXmlStream());

        SessionFactory newSessionFactory = cfg.buildSessionFactory(serviceRegistry);

        //保存对象
        Session newSession = newSessionFactory.openSession();
        Transaction tx = newSession.beginTransaction();
        for (int i = 0; i < 5; i++) {
            Map<String, Object> student = new HashMap<>();
            //student.put("id", i);
            student.put("username", "张三" + i);
            student.put("password", "adsfwr" + i);
            student.put("sex", i % 2 == 0 ? "male" : "female");
            student.put("age", i);
            student.put("birthday", new Date());
            newSession.save("dyc_Student", student);
        }
        //查询所有对象
        Query query = newSession.createQuery("from dyc_Student");
        List list = query.getResultList();
        System.out.println("resultList: " + list);
        tx.commit();
        newSession.close();

可以深入研究后发现,因为SessionFactory一旦创建内部状态就不可变了(官方文档),所以只能为每个动态加载的实体创建一个单独的SessionFactory(动态实体多了后的管理和内存问题),或者重建SessionFactory把之前的和新增的包含进来(关闭SessionFactory时万一有未完成的事务)。 但是无论哪个选择在实际应用场景中都不现实。

Q.E.D.


我并不是什么都知道,我只是知道我所知道的。