前言
Hibernate 作为一种全自动 ORM 框架,在几年前常与 Spring、Struts2 一起使用,并称 SSH,作为主流的企业级应用框架。伴随着 MyBatis 的诞生,以及 Hibernate 本身的一些缺陷,如今 Hibernate 已经慢慢淡出了大家的视野。
然而作为 Java 持久化规范 JPA 的一种实现,日常开发中,我们偶尔还能听到它的声音。有时我们还会将其与 MyBatis 做一些对比,不过由于很多同学没有使用过它,可能查阅一些资料后还是云里雾里的,因此有必要对它有一个入门级的认识。
本文的目的并非谈论其缺陷,你能收获的是对 Hibernate 的整体认识。
快速上手
让我们先通过一个实例的方式对 Hibernate 有一个初步的认识。
首先引入相关依赖,这里我们使用的是 MySQL 数据库。
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.6.9.Final</version> </dependency>
假定我们在数据库中有一个 user
表定义如下。
create table user ( id bigint unsigned auto_increment comment '主键' primary key, username varchar(20) not null comment '用户名', password varchar(20) not null comment '密码' );
对应到 Java 中,应该有一个实体类与之对应。
@Data public class User { private Long id; private String username; private String password; }
作为 ORM 框架,需要将数据库表和实体类之间建立映射关系。最初 Hibernate 使用 xml 文件表示映射关系,sun 公司参考 Hibernate 制定 JPA 规范后,Hibernate 又反向实现了 JPA 规范,而后 Hibernate 支持了使用 JPA 规范中的注解表示映射关系。这里我们使用 Hibernate 的 xml 文件表示映射关系。
表示 user 表与 User 类映射关系的 User.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 name="com.zzuhkp.hibernate.entity.User" table="user"> <id name="id" column="id"> <generator class="native"/> </id> <property name="username" column="username"/> <property name="password" column="password"/> </class> </hibernate-mapping>
最后,我们还需要对 Hibernate 进行一些全局的配置,例如 JDBC 的连接信息、日志打印等。
配置文件 hibernate.cfg.xml
内容如下。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <!--连接配置--> <property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property> <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/test</property> <property name="hibernate.connection.username">root</property> <property name="hibernate.connection.password">12345678</property> <!--SQL 打印--> <property name="hibernate.show_sql">true</property> <property name="hibernate.format_sql">true</property> <!--映射文件--> <mapping resource="/hibernate/mapping/User.hbm.xml"/> </session-factory> </hibernate-configuration>
有了必要的配置后,我们就可以对数据库进行操作了。
@Test public void testSession() { Configuration configuration = new Configuration(); configuration.configure("/hibernate/cfg/hibernate.cfg.xml"); SessionFactory sessionFactory = configuration.buildSessionFactory(); Session session = sessionFactory.openSession(); Transaction transaction = session.beginTransaction(); try { User user = new User(); user.setUsername("hkp"); user.setPassword("123"); session.save(user); transaction.commit(); } catch (Exception e) { log.error(e.getMessage(), e); transaction.rollback(); } finally { session.close(); } sessionFactory.close(); }
这里只是简单的向 user 表中添加了一条数据。
核心组件
通过示例,我们可以看出 Hibernate 有如下的一些核心组件。
1. Configuration
Configuration 是 Hibernate xml 配置文件的 Java 类表示,如果有一些配置不方便在 xml 配置文件中表示,还可以直接使用 Configuration 类的 API 直接配置。
通常配置中会包含一些影响 Hibernate 运行的属性配置、映射文件配置。
Hibernate 可配置属性实在太多了,影响内部组件运行的属性全部一股脑放在了一起,因此个人感觉设计不是太友好,也许和 MyBatis 一样,把属性配置下沉到表示内部组件的标签中更为合适。
2. SessionFactory
SessionFactory 是 Session 的工厂类,对于项目中的一个数据库来说,根据配置创建一个 SessionFactory 对象即可。
3. Session
Session 表示 Hibernate 与数据库之间的一次会话,类似于 Connection,提供了多种方式操作数据库,非线程安全,通常来说,会为每个请求单独创建一个 Session 对象。这是 Hibernate 最核心的一个类。
4. Transaction
Transaction 是 Hibernate 中进行事务操作的接口,在 Hibernate 中可以使用基于 JDBC 的事务和基于 JTA 的事务,上层对事务的实现是无感的。
5. Hibernate 5 新组件
由于 Configuration 设计的复杂性及局限性,Hibernate 设计了一套新的组件替换 Configuration 创建 SessionFactory。最简单的构建示例代码如下。
// 1. 构建 ServiceRegistry StandardServiceRegistryBuilder standardServiceRegistryBuilder = new StandardServiceRegistryBuilder(); standardServiceRegistryBuilder.configure("/hibernate/cfg/hibernate.cfg.xml"); ServiceRegistry serviceRegistry = standardServiceRegistryBuilder.build(); // 2. 构建 Metadata MetadataSources sources = new MetadataSources(serviceRegistry); Metadata metadata = sources.buildMetadata(); // 3. 构建 SessionFactory SessionFactory sessionFactory = metadata.buildSessionFactory();
可以看到,即便用最简单的方式构建 SessionFactory,也涉及到了多个类。在 API 设计上也许更合理,但是对于使用方来说却造成了诸多不便。整个 Hibernate 框架提出了太多的概念,相对于 MyBatis 来说确实重了不少,上手难度极高。
对上述构建 SessionFactory 过程做一个简单的总结。
第一步是构建 ServiceRegistry,这个接口用于对 Hibernate 内部使用的服务进行注册,通常使用 StandardServiceRegistryBuilder 构建,构建时可以设置配置文件在类路径下的地址。
第二步是构建 Metadata,这个接口内部包含了实体和数据库表的映射关系,使用 MetadataSources 进行必要的配置和构建。
最后使用 Metadata 构建 SessionFactory。
每个步骤都可以进行细化,添加更多的配置,以 MetadataSources 为例,我们可以使用如下的方式添加映射文件。
MetadataSources sources = new MetadataSources(serviceRegistry); sources.addResource("/hibernate/mapping/User.hbm.xml"); Metadata metadata = sources.buildMetadata();
常用配置
数据库访问
JDBC 数据源
ORM 框架必须要配置的是数据库连接,而连接一般会配置一个支持连接池的数据源。
Hibernate 内部默认实现了一个连接池,同时也支持多种开源的 DataSource 实现,包括 c3p0、Proxool、HikariCP、Vibur DBCP、Argoal,如果这些不满足要求还可以定义自己的实现。
以目前比较常用的 HikariCP 为例,使用方式如下。
首先引入依赖。
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-hikaricp</artifactId> <version>5.6.9.Final</version> </dependency>
然后在配置文件中添加必要的配置即可。
<!--通用配置,针对多种数据源有效--> <property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property> <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/test</property> <property name="hibernate.connection.username">root</property> <property name="hibernate.connection.password">12345678</property> <!--HikariCP 特有配置--> <property name="hibernate.hikari.idleTimeout">600000</property> <property name="hibernate.hikari.minimumIdle">5</property> <property name="hibernate.hikari.maximumPoolSize">10</property>
JTA 事务管理
除了对 JDBC 的支持,Hibernate 还添加了对 JTA 事务管理的支持。以 JTA 的实现 atomikos 为例,首先需要引入依赖。
<dependency> <groupId>com.atomikos</groupId> <artifactId>transactions-jdbc</artifactId> <version>4.0.6</version> </dependency>
然后在配置文件中添加如下 JTA 的配置。
<!--设置 JTA 控制事务--> <property name="hibernate.transaction.coordinator_class">jta</property> <!--JTA 事务管理器配置--> <property name="hibernate.transaction.jta.platform">Atomikos</property> <!--需要时获取 Connection,Session 关闭时释放 Connection--> <property name="hibernate.connection.handling_mode">DELAYED_ACQUISITION_AND_HOLD</property> <!--Session 与线程绑定--> <property name="current_session_context_class">jta</property>
📢注意:由于 atomikos 有自己独有的数据源配置,因此上面示例配置文件中的数据源配置需要删去,然后使用 API 单独配置数据源实例。
private static DataSource getDataSource() { Properties properties = new Properties(); properties.put("url", "jdbc:mysql://127.0.0.1:3306/test"); properties.put("user", "root"); properties.put("password", "12345678"); AtomikosDataSourceBean ds = new AtomikosDataSourceBean(); ds.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource"); ds.setXaProperties(properties); ds.setUniqueResourceName("resourceName"); ds.setPoolSize(10); ds.setBorrowConnectionTimeout(60); return ds; } Configuration configuration = new Configuration(); configuration.configure("/hibernate/cfg/hibernate.cfg.xml"); // JTA 数据源 configuration.getProperties().put(AvailableSettings.DATASOURCE, getDataSource());
由于 Hibernate 抽象出了事务接口 Tranaction
,因此事务使用方式和 JDBC 保持一致。
日志
关于日志,常用场景是开发阶段查看打印的 SQL,配置文件中常使用如下的两个属性配置
<property name="hibernate.show_sql">true</property> <property name="hibernate.format_sql">true</property>
。其中 hibernate.show_sql
用于配置 SQL 打印,hibernate.format_sql
则用于对打印的 SQL 格式化。
不过有时我们可能还想要看设置的参数值,此时需要借助具体的日志框架来实现。比较常用日志框架是 logback,引入依赖。
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency>
然后在 logback 配置文件 logback.xml
中做如下配置就可以查看 SQL 参数了。
<?xml version="1.0" encoding="UTF-8"?> <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <Pattern>%d{yyyy-MM-dd_HH:mm:ss.SSS} [%-5level] [%logger{36}] - %msg%n</Pattern> </encoder> </appender> <logger name="org.hibernate.type" level="trace"/> <root level="info"> <appender-ref ref="STDOUT"/> </root> </configuration>
主要是把 org.hibernate.type 日志级别设置为 trace。
模式生成
模式生成用于根据某些规则创建或修改数据库表。
1. 映射关系生成表结构
配置文件中有一个属性 hibernate.hbm2ddl.auto 可用于自动根据配置的映射关系生成数据库表结构,可取值及含义如下。
可以看到,除了 none 和 validate,其他的取值都会导致数据库表的变动,而我们一般不会单节点部署服务,因此其他的取值更适合单机应用或在测试环境使用。
2. 脚本文件执行
Hibernate 允许在应用启动时执行给定的脚本文件。假定类路径下有一个脚本文件 schema-generation.sql 内容如下。
insert into user(username,password) values ('hkp','123');
如果想要执行这个脚本文件,可以在配置文件中指定。
<property name="hibernate.hbm2ddl.import_files">schema-generation.sql</property>
这样,当应用启动时就会执行我们的脚本文件了,不过可以看出,这仅适用于单机应用单次执行,为避免多节点重复执行,建议仅在测试环境使用。
3. 数据库对象
如果不想创建脚本文件,还可以在映射文件指定 SQL。
<hibernate-mapping> <database-object> <create>...</create> <drop>...</drop> </database-object> </hibernate-mapping>
4. 其他映射定义
模式生成时除了映射关系,还可指定一些约束,例如是否唯一、默认值等。示例如下:
<class name="com.zzuhkp.hibernate.entity.User" table="user"> <property name="username" column="username" unique="true"/> <property name="deleted"> <column name="deleted" default="0"/> </property> </class>
体管理
作为一个 ORM 框架,Hibernate 除了处理映射关系,还会把从数据库查询到的数据在内存中统一的管理,管理的对象被称为领域模型,个人比较倾向于使用实体表达。
缓存
在 Hibernate 中,缓存被分为基于 SessionFactory 的一级缓存和基于 Session 的二级缓存。
一级缓存主要用于 Hibernate 跟踪管理的实体的状态变化,在某个时间 Hibernate 会把实体的变化提交到数据库。一级缓存不可关闭,也可用于避免重复从数据库加载数据。
二级缓存是常规意义上的缓存,Hibernate 提供的实现主要基于内存,不适用于多节点,由于缓存可能与数据库不一致,因此个人不推荐使用,限于篇幅这里也不加介绍。可通过如下的配置属性关闭二级缓存。
<property name="hibernate.cache.use_second_level_cache">false</property>
实体状态
Hibernate 管理的实体有自己的生命周期,不同的操作会操作其状态发生不同的变化,我这里画了张图,如下所示。
实体的状态有4种,包括临时、持久、游离、删除。
刚创建的实体处于临时状态。
创建的实体被保存到数据库后,这个实体就变成持久状态被 Hibernate 管理起来。
持久状态的实体被删除后就处于删除状态,待 Hibernate 把状态变化同步到数据库后就会真正删除数据。
Session 关闭后持久状态的实体就会变成游离状态,当然了也可以手动清理 Session。通过 Session 的某些方法还可以将游离状态的实体转换为持久状态。
实体修改后,Hibernate 不一定马上把变动同步到数据库,何时同步可以通过 Session.setFlushMode 或 Session.setHibernateFlushMode 方法设置,默认情况使用 FlushMode.AUTO 自动刷新模式, 事务提交、 HQL 或 SQL 执行时都可能触发同步。
用户可以手动调用 Session.flush 方法将变动刷新到数据库,也可以手动调用 Session.refresh 将最新的数据刷新到管理的实体中。
数据库操作
Hibernate 提供了 4 种操作数据库的方式,分别是 Session API 、Criteria API、HQL 和 SQL。
Session API
Sesion API 主要用于对单条记录进行增删改查。还是以快速上手章节部分的数据库表为例,这里不再赘述相关配置。
1. 新增
新增记录直接调用 Sesion.save 方法即可,如果主键由数据库生成,Hibernate 会自动设置主键值到对应的实体字段中。示例如下:
session.save(user);
2. 删除
删除记录直接调用 Session.remove
方法即可,需要注意的是方法参数值必须为 Hibernate 管理的持久状态的实体,这也意味着必须先查询,然后再删除,因此不太灵活。示例如下:
session.remove(user);
3. 修改
修改操作调用 Session.update
方法即可,不限制实体是否处于持久状态,如果失败将抛出异常。
session.update(user);
4. 查询
关于查询,Session 支持根据单个 ID 查询或根据多个 ID 查询,示例如下。
User user = session.get(User.class, 24L); User user1 = session.load(User.class, 25L); User user2 = session.byId(User.class).load(26L); List<User> users = session.byMultipleIds(User.class).multiLoad(Arrays.asList(27L, 28L));
根据 Hibernate 官网的介绍,Session.load 方法可以通过创建代理的方式延迟加载数据,实际测试未发现有此效果。
通过上面的示例可以发现,Session API 只能对单条记录进行简单的操作,因此不适用于复杂的场景。
Criteria API
Hibernate 提供了重载的 Session.createCriteria 方法用于生成 Criteria 接口实例,用户可以使用 Criteria 提供的方法执行数据库查询,这种方式支持单表也支持多表联查。
为了兼容 JPA,到了 Hibernate 5.2 版本,createCriteria 方法被废弃,转而使用 JPA 的方法替代,到了 6.0 版本,这个方法已经被删除。不过由于 Oracle 将 Java EE 移交给 Eclipse 基金会后要求对方不能再使用 javax 包名,Hibernate 6.0 实现的最新的 JPA 包名已经发生变化,为了减少影响很多项目仍然停留在 Hibernate 5.x 版本。
这里我们先看下 Hibernate Criteria 的使用方式,下篇介绍 JPA 时再着重介绍 JPA 最新的方法。一个相对复杂的分页查询示例如下。
List<User> list = session.createCriteria(User.class) .add(Restrictions.eq("username", "hkp")) .addOrder(Order.desc("id")) .setFirstResult(10) .setMaxResults(10) .list();
等价于如下的 SQL:
select id,username,password from user where username = 'hkp' order by id desc limit 10,10
HQL
HQL 全称 Hibernate Query Language,是一个以对象为中心的查询语言,语法类似于 SQL,与上述 Criteria
示例等同的代码如下。
List<User> list = session.createQuery( "select u from User u where u.username = :username order by u.id desc") .setParameter("username", "hkp") .setFirstResult(10).setMaxResults(10) .list();
SQL
如果 Criteria
或 HQL
不满足要求,还可以使用 SQL,上述 HQL 等同的 SQL 代码如下。
List<User> list = session.createNativeQuery( "select * from user as u where u.username = :username order by u.id desc") .setParameter("username", "hkp") .setFirstResult(0) .setMaxResults(10).list();
拦截器与事件
拦截器在 Session
执行某些方法前被回调,一个比较常用的场景设置创建人、创建时间、修改人、修改时间,另外还可以使用拦截器进行加解密。拦截器示例如下。
public class CustomInterceptor extends EmptyInterceptor { @Override public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { // insert,加密、设置创建人、创建时间 this.setCreateInfo(entity, state, propertyNames); return true; } @Override public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) { // update,加密、设置修改人、修改时间 this.setUpdateInfo(entity, currentState, propertyNames); return true; } @Override public boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { // select,解密 return super.onLoad(entity, id, state, propertyNames, types); } ...省略 setCreateInfo、setUpdateInfo 方法代码 }
定义之后进行简单配置就可以了。
configuration.setInterceptor(new CustomInterceptor());
除了拦截器,Hibernate 还提供了事件机制,作为拦截器的补充,不同的 Session
接口方法执行时会发出不同的事件,使用事件机制替代上述拦截器的功能可以使用如下的代码。
EventListenerRegistry eventListenerRegistry = this.sessionFactory.unwrap(SessionFactoryImplementor.class) .getServiceRegistry().getService(EventListenerRegistry.class); eventListenerRegistry.prependListeners(EventType.POST_LOAD, new CustomPostLoadEventListener()); eventListenerRegistry.prependListeners(EventType.SAVE, new CustomSaveEventListener()); eventListenerRegistry.prependListeners(EventType.UPDATE, new CustomUpdateEventListener());
映射关系
映射关系可以说是 Hibernate 最复杂的部分了,Hibernate 不仅支持简单类型到数据库列的映射,还支持复杂的实体关系以及面向对象中的继承关系到数据库的映射。
映射类型
Hibernate 中的映射关系主要从面向对象的角度进行划分,Hibernate 中的类型分为值类型和实体类型。
向前面示例中的 User 类就是一个实体类型,有自己的标识符,可以独立于其他对象存在。
值类型则必须依托于实体类型,其生命周期和实体类型保持一致。值类型又可以进一步划分为基本类型、嵌入类型、集合类型,下面分别介绍。
命名策略
在介绍映射关系之前首先要提到的是命名策略,在使用 MyBatis 的时候,为了便于将数据库表字段转换为实体属性,我们常常会进行下划线转驼峰的配置,Hibernate 也有类似的策略。
Hibernate 的命名策略主要用于在映射关系中未显式指定实体类型、字段对应的数据库表、列名时自动进行的转换策略。
在 Hibernate 5 之前有一个 NamingStrategy 接口用于定义命名策略,不过在 Hibernate 5 版本的时候这个接口被废弃,转而替换为了隐式命名策略 ImplicitNamingStrategy 和物理命名策略 PhysicalNamingStrategy。Hibernate 首先使用隐式命名策略获取逻辑名称,然后再使用物理命名策略将逻辑名称转换为数据库中真正存在的物理名称。
Spring Boot 就提供了隐式命名策略和物理命名策略的实现,显式配置表名、列名的情况下用户无需关心命名策略。如果需要修改有两种方式。
指定配置属性 hibernate.implicit_naming_strategy 或 hibernate.physical_naming_strategy。
调用方法 Configuration.setImplicitNamingStrategy 或 Configuration.setPhysicalNamingStrategy。
基本类型映射
基本类型用于单个数据库列与实体属性之间的映射。Hibernate 内置了多种基本类型,支持了包括 Java 基本类型、日期类型在内的类型。
对于内置支持的类型,在 xml 映射文件中通过 property 标签配置实体属性名称与数据库列名之间的映射关系即可,Hibernate 知道如何进行类型处理。
<property name="username" column="username"/>
不过如果想要将自定义的 Java 类型与数据库列之前进行转换,就需要手动向 Hibernate 注册类型了。假定 user 表有一个 varchar 类型的 interests 列存储用户的兴趣爱好,我们想把它转换为 Java 中的 List 类型,可以如下操作。
首先自定义一个 Hibernate 类型,此时需要继承 BasicType 接口。
public class StringListType extends AbstractSingleColumnStandardBasicType<List> implements DiscriminatorType<List> { public static final StringListType INSTANCE = new StringListType(); public StringListType() { super(VarcharTypeDescriptor.INSTANCE, StringListTypeDescriptor.INSTANCE); } @Override public List stringToObject(String xml) throws Exception { return fromString(xml); } @Override public String objectToSQLString(List value, Dialect dialect) throws Exception { return toString(value); } @Override public String getName() { return "StringList"; } @Override protected boolean registerUnderJavaType() { return true; } }
然后向 Hibernate 注册。
configuration.registerTypeContributor((typeContributions, serviceRegistry) -> typeContributions.contributeType(StringListType.INSTANCE));
最后在映射文件中指定即可。
<property name="interests" column="interests" type="com.zzuhkp.hibernate.type.StringListType"/>
嵌入类型映射
嵌入类型可以为普通的 Java 类型,也可以是 Java 集合类型、Map。普通的 Java 类型也被称为组件,集合类型则用于表示一对多、多对一、多对多的关系。
由于嵌入集合类型较为复杂,这里看下组件类型,组件常用于对实体的属性进行分组,假如我们想使用如下的形式表示用户。
@Data public class User { private Long id; private String username; private String password; private UserExt ext; } @Data public class UserExt { private String name; private Integer age; }
这里的 User
类型中的 UserExt
就是一个组件,我们可以使用如下的映射文件表示。
集合类型映射
这里的集合类型与嵌入类型中的集合类型很相似,从面向对象的角度来看,只是这里的集合类型中的元素是 Hibernate 管理的实体,而嵌入类型中的集合类型只是作为实体中的属性出现,具体到数据库表,其实没什么两样。
集合类型映射可用于表示一对多、多对一、多对多的关系,在 Hibernate 中,还可以用多对一表示一对一的关系。
集合类型也是 Hibernate 中比较复杂的部分,限于篇幅这里仅给出一对多的示例。
假定用户可以有多个收货地址,收货地址数据库表如下。
create table address ( id bigint unsigned auto_increment comment '主键' primary key, user_id bigint unsigned null comment '用户ID', province_name varchar(20) null comment '省份名称', city_name varchar(20) null comment '城市名称', area varchar(20) null comment '区域', detail varchar(100) null comment '详细地址' );
对应实体如下。
@Data public class Address{ private Long id; private String provinceName; private String cityName; private String area; private String detail; }
在 User
类型中可以引入地址列表。
@Data public class User { private Long id; private String username; private String password; private Set<Address> addresses; }
映射关系可以如下表示。
<class name="com.zzuhkp.hibernate.entity.User" table="user"> <id name="id" column="id"> <generator class="native"/> </id> <property name="username" column="username"/> <property name="password" column="password"/> <set name="addresses" inverse="false" lazy="true"> <key column="user_id"/> <one-to-many class="com.zzuhkp.hibernate.entity.Address"/> </set> </class>
像操作基本类型一样操作集合类型即可。
总结
从上面的内容可以看出,仅仅是最基础的 Hibernate 内容也涉及到了大量的概念,个人看来 Hibernate 的缺点包括但不限于上手难度极高、设计不够灵活、不适用微服务盛行的今天,而 MyBatis 就相对轻量化,使用也比较简单,这也是 MyBatis 能取代 Hibernate 的原因。