前言: 之前的一个项目有个功能需要运行时创建/更新表结构,当时第一时间想到的是根据字段和表名拼接建表和更新表的sql,现在知道Hibernate有个动态模型(dynamic models)可以完成此功能,遂记录下。
Hibernate Dynamic models
官方文档
文档原话: 运行期的持久化实体没有必要一定表示为像POJO类或JavaBean对象那样的形式。Hibernate也支持动态模型 (在运行期使用Map的Map)和象DOM4J的树模型那 样的实体表示。使用这种方法,你不用写持久化类,只写映射文件就行了。
使用Hibernate的内置api来更新表
例子:
- 先定义一个映射文件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方言配置。
后续
- 该方案的优点:
可以定义个Hibernate模版文件,运行时用FreeMarker等模版引擎动态生成相应的映射文件流,这样比自己去写sql拼接优雅的多。而且还不用考虑数据库差异。 - 我看官方示例对动态模型能用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.