Hibernate 快速入门1 - 基本用法
Hibernate简介
ORM:对象关系映射(ORM, Object Relational Mapping)。用于实现面向对象编程语言里不同类型系统的数据之间的转换。
Hibernate: Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的orm框架,hibernate可以自动生成SQL语句,自动执行。
一句话:让程序员以面向对象的方式访问数据库。
注意:本文假设你对JDBC和Java反射有基本的了解,熟悉基本MySQL操作。
0 下载
// hibernate我使用的是hibernate-release-5.2.16.Final
hibernate下载:http://hibernate.org/orm/releases/
mysql驱动:https://dev.mysql.com/downloads/connector/j/
common-logging: http://commons.apache.org/proper/commons-logging/
解压hibernate,将/path-to-hibernate/lib/required、mysql驱动所在目录和common-logging 所在目录添加到classpath。
在Intellij Idea中,可以在Edit -> Project Structure -> Libraries中配置。
1.1 简单例子
先考虑下,传统访问数据库的方式是:编写SQL,获取数据,赋值给对象。Hibernate要做的就是这一步。
先把数据库配置好:
@ mysql -u root -p
@ CREATE USER hibernate IDENTIFIED BY 'hibernate123';
@ CREATE DATABASE hibernatedb;
@ USE hibernatedb;
@ GRANT ALL ON hibernatedb.* TO hibernate;
上面在MySQL中为本文建立了一个用户: hibernate
,密码是: hibernate123
,数据库是: hibernatedb
首先来看一个例子:
// src/com/example/test/Student.java
package com.example.test;
import javax.persistence.*;
@Entity
@Table(name = "student_info")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "name")
private String name;
private int age;
@Transient
private boolean olderThan18;
@Override
public String toString() {
return "id: " + id + ", name: " + name + ", age: " + age;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
// src/com/example/test/HibernateTest.java
package com.example.test;
// user: hibernate, pass: hibernate123, db:hibernatedb
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Configuration;
public class HibernateTest {
public static Integer addStudent() {
Configuration conf = new Configuration().configure();
StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure().build();
try (SessionFactory sessionFactory = conf.buildSessionFactory(registry);
Session session = sessionFactory.openSession()) {
Transaction transaction = session.beginTransaction();
Student student = new Student();
student.setAge(18);
student.setName("robert");
Integer id = (Integer) session.save(student);
transaction.commit();
return id;
}
}
public static void searchStudent(Integer id) {
if (id != null) {
Configuration conf = new Configuration().configure();
StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure().build();
try (SessionFactory sessionFactory = conf.buildSessionFactory(registry);
Session session = sessionFactory.openSession()) {
Transaction transaction = session.beginTransaction();
Student student = session.get(Student.class, id);
if (student != null) {
System.out.println(student);
}
transaction.commit();
}
}
}
public static void main(String[] args) {
Integer id = addStudent();
searchStudent(id);
}
}
src/hibernate.cfg.xml
<?xml version='1.0' encoding='utf-8'?>
<!--
~ Hibernate, Relational Persistence for Idiomatic Java
~
~ License: GNU Lesser General Public License (LGPL), version 2.1 or later.
~ See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
-->
<!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>
<!-- Database connection settings -->
<property name="connection.driver_class">com.mysql.cj.jdbc.Driver</property>
<!--后面根据tomcat,修改成根据JDNI来访问。参考java ee 企业应用实战 p393-->
<property name="connection.url">jdbc:mysql://localhost:3306/hibernatedb</property>
<property name="connection.username">hibernate</property>
<property name="connection.password">hibernate123</property>
<property name="hibernate123"/>
<!-- maximum number of connections -->
<property name="hibernate.c3p0.max_size">20</property>
<property name="hibernate.c3p0.min_size">1</property>
<property name="hibernate.c3p0.max_statements">30</property>
<property name="hibernate.c3p0.idle_test_period">3000</property>
<property name="hibernate.c3p0.acquire_increment">2</property>
<property name="hibernate.c3p0.valid">true</property>
<!-- SQL dialect -->
<!--caution. it's not MySQLInnoDBDialect, but MySQL5InnoDBDialect !!-->
<property name="dialect">org.hibernate.dialect.MySQL55Dialect</property>
<property name="hbm2ddl.auto">update</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<property name="hibernate.format_sql">true</property>
<mapping class="com.example.test.Student"/>
</session-factory>
</hibernate-configuration>
下面运行程序,可以看到console有部分输出为:
Hibernate:
select
student0_.id as id1_0_0_,
student0_.age as age2_0_0_,
student0_.name as name3_0_0_
from
student_info student0_
where
student0_.id=?
id: 1, name: robert, age: 18
前面是Hibernate生成的SQL语句。id: 1, name: robert, age: 18
就是我们新建到数据库中数据。
mysql> show tables;
+-----------------------+
| Tables_in_hibernatedb |
+-----------------------+
| student_info |
+-----------------------+
7 rows in set (0.01 sec)
mysql> select * from student_info;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 18 | robert |
+----+-----+--------+
1 row in set (0.00 sec)
这表明数据库中已经有一个名为student_info的表,表中有我们插入的元组。
1.2 基本使用
下面来讲解一下hibernate的基本使用。
1.2.1 类的注解
拿前面例子来说明。
@Entity: 标识这个类想要被映射到数据库中。
@Table: 指定表的名字。如果省略@Table,表的名字则是这个类的类名(the unqualified class name)。这里表的名字是student_info。如果上面省略@Table,表名是Student,并不是com.example.test.Student。
@Id:标识这个字段用作主键(primary key)。可以把多个字段都用@Id标识,对应属性的组合构成了主键。多个@Id不是JPA(Java Persistence API)所支持的。JPA自己查。这里就只用一个字段。
@GeneratedValue: 表示主键生成的策略。比如自增还是生成一个UUID(这里是Integer不能用UUID)。strategy = GenerationType.IDENTITY
生成策略是由数据库自动生成。例子中是自增。其他策略(比如GenerationType.AUTO等)请参考文档。
@Column: 设置属性在数据库表中的名字。如果省略,就是属性全部小写的名字。
@Transient: 表示该属性不应该被实例化。
最重要的:Student是一个bean。它为所有需要让外部访问的成员变量都提供了setter和getter。除了static和@transient标记的属性外,其他属性所有不管有没有annotation,都会被持久化。
请查看官方文档,看这些注解还可以设置哪些属性。
也就是如下:
@Entity
public class Student {
private String hometown; // will be persistent too.
};
1.2.2 配置文件
除了要在源代码中用注解指出哪些class需要被映射外,我们还要配置文件。Hibernate的配置文件是hibernate.cfg.xml
。基本的数据库配置就像上面举例的一样,数据库的用户名,密码,db名字。hibernate.c3p0.这些属性是数据库连接*的一些基本信息,看名字基本能看懂。不懂的查阅文档。
为了让hibernate能根据不同的数据库进行优化,我们还要设置数据库的种类,我用的是MySQL5,所以设置方言为MySQL5. <property name="dialect>org.hibernate.dialect.MySQL55Dialect</property>
其他的数据库怎么设置请查阅文档。
<property name="hbm2ddl.auto">update</property>
是让hibernate根据需要更新数据库(包含了创建)。hbm2ddl.atuo还可以有其他值,请查阅温度。注意,这个值最好仅在开发测试的时候使用,正式环境中请勿使用这个property。
配置最下面<mapping/>指定,哪些类需要被映射。这个容易被忽略。
1.2.3 SessionFactory
有了被注解的类,有了配置文件,我们还要进行一系列的操作。
- 加载配置
- 启动
- 调用SessionFactory得到一个Session
- 开始事务
- 程序逻辑处理
- 结束事务
- 关闭Session和SessionFactory
分别如下:
Configuration conf = new Configuration().configure(); // 默认加载src/hibernate.cfg.xml
StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure().build(); // 启动
try (SessionFactory sessionFactory = conf.buildSessionFactory(registry);
Session session = sessionFactory.openSession()) { // 打开得到session
Transaction transaction = session.beginTransaction(); // 开始事务
Student student = session.get(Student.class, id); // 逻辑处理
if (student != null) {
System.out.println(student);
}
transaction.commit(); // 结束事务
} // 关闭Session和SessionFactory
注意:要正确关闭Session和SessionFactory,不然程序退出不了。
我们对数据库的CRUD操作都是先打开一个事务,然后通过Session来进行。即使只查询,不修改数据库,事务也是必不可少的。具体原因请看这里https://stackoverflow.com/questions/13539213/why-do-i-need-transaction-in-hibernate-for-read-only-operation
1.2.4 对象的状态
对象在Hibernate中有如下几个状态:
- transient: never persistent, not associated with any Session
- persistent: associated with a unique Session
- detached: previously persistent, not associated with any Session
暂时分别翻译为:瞬态,持久态,脱管态。
瞬态的意思是,这个对象的被new以后,从来没有被持久化到数据库中。
持久态的意思是,这个对象和一个session是相关联的。比如刚从数据库中取出来:调用session.get()
脱离态的意思是,这个对象被持久化过,但现在没有和一个session相关联。比如对持久态的对象调用:obj.evict,或者session.close()。
如下:
1.2.4 基本函数
public interface Session {
Serializable save(Object object);
void saveOrUpdate(Object object);
void persist(Object object);
<T> T load(Class<T> theClass, Serializable id);
<T> T get(Class<T> entityType, Serializable id);
void update(Object object);
void delete(Object object);
void evict(Object object);
// ... other methods
}
保存:save、saveOrUpdate和persist返回的是生成的主键(the generated identifier)。它们主要是把瞬态的对象持久化。其中save会返回生成的主键。在我这个版本的hibernate,对于处于持久态的对象,不管对象是否有更新,再次调用save后,似乎数据库都是一样的。
查询: load和get。区别是如果数据库中,没有id对应的记录,那么load会抛出异常,get会返回null。
更新:update只能作用于处于持久态和脱离态的对象上。对于脱离态的对象,这个操作还会把对象变为持久态。
删除:delete。很简单,只能删除处于持久态的对象。
evict: 把对象从持久态转化为脱离态。
还有一些其他函数,请参考官方文档。
1.3 映射集合属性
如果一个属性有多个地址,也就是Student的属性为一个集合的时候。怎么办呢?
1.3.1 映射 list, set, array
首先,为了避免数据冗余,这些bean属性的集合,肯定要保存在另外一个表中。比如(id, name, addressList),如果addressList让然和student保存在一个表中,就会产生冗余: (1, "robert", "beijing"), (1, "robert", "shanghai")。 后面MySQL的输出也验证了这个想法。
映射list(数组和list一样):
@Entity
@Table(name = "student_info")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "name")
private String name;
private int age;
@ElementCollection(targetClass = String.class) // element class type
// name = "addresses": name of table contain elements of list is address.
// joinColumns = @JoinColumn(..): FOREIGN KEY (student_id) REFERENCES Student (id).
// In fact, we can rename Student.id by using @Column(name = "student_id").
// And in such case we can omit joinColumns here.
@CollectionTable(name = "addresses", joinColumns = @JoinColumn(name = "student_id", referencedColumnName = "id"))
@Column(name = "address", nullable = false) // name of column representing elements of the set
@OrderColumn(name = "addr_id")
private List<String> addressList;
// getters and setters
}
HibernateTest.java.addStudent
session.beginTransaction();
Student student = new Student();
student.setName("robert");
student.setAge(18);
List<String> addrs = new ArrayList<>(2);
addrs.add("bj");
addrs.add("sh");
student.setAddressList(addrs);
session.save(student);
session.getTransaction.commit();
数据库的输出:
mysql> desc addresses;
+------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| Student_id | int(11) | NO | PRI | NULL | |
| address | varchar(255) | NO | | NULL | |
| addr_id | int(11) | NO | PRI | NULL | |
+------------+--------------+------+-----+---------+-------+
3 rows in set (0.00 sec)
mysql> select * from addresses;
+------------+---------+---------+
| Student_id | address | addr_id |
+------------+---------+---------+
| 7 | bj | 0 |
| 7 | sh | 1 |
+------------+---------+---------+
2 rows in set (0.00 sec)
mysql> show create table addresses;
CREATE TABLE `addresses` (
`student_id` int(11) NOT NULL,
`address` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`addr_id` int(11) NOT NULL,
PRIMARY KEY (`student_id`,`addr_id`),
CONSTRAINT `FK37pobof33ovjb9oxnt205houy` FOREIGN KEY (`student_id`) REFERENCES `student_info` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
映射集合用@ElementCollection
注解。
还有个@CollectionTable
注解,name字段指定指定集合对应的表的名字,joinColumns
是一个数组类型,它的元素类型为@JoinColumn
.
@JoinColumn
:专门用来指定外键,它的name属性指定它所属于的表对应的名字,referencedColumnName指定被引用表的属性名。举个例子: @JoinColumn(name = "student_id", referencedColumnName = "id")
表示在@JoinColumn所在的表建立一个属性student_id, 同时建立一个外键约束: FOREIGN KEY (student_id) REFERENCES student_info (id);
@OrderColumn(name = "addr_id")
表示addr_id这个列用来保存元素的顺序。
可以在MySQL输出中看到的确建立了对应的属性和外键约束。 addresses.student_id <-> student_info.id。需要注意的是,addresses上的外键约束,默认是拒绝更改。意思是,如果删除student_info中id=7的行,这个操作会被拒绝。如下:
mysql> delete from student_info where id = 7;
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`hibernatedb`.`addresses`, CONSTRAINT `FK37pobof33ovjb9oxnt205houy` FOREIGN KEY (`student_id`) REFERENCES `student_info` (`id`))
如果我们通过hibernate删除id = 7 的Student,那么他对应的address确可以被删除。
Integer id = 7;
Transaction transaction = session.beginTransaction();
Student student = session.get(Student.class, id);
if (student != null) {
System.out.println("deleting " + student);
session.delete(student);
}
transaction.commit();
数据库的输出:
mysql> select * from addresses;
Empty set (0.00 sec)
这是为什么呢?仔细查看控制台,我们发现两条sql语句:
deleting id: 7, name: robert, age: 18, addrs: [bj, sh]
Hibernate:
delete
from
addresses
where
student_id=?
Hibernate:
delete
from
student_info
where
id=?
这下非常明显了。先删除了addresses表中 id = 7的行,再删除student_info中id = 7的行。
映射set的话,和上面类似,只是不能再用@OrderColumn。
当我们有疑惑的时候,仔细查看hibernate控制台的输出是一个好方法。
1.3.2 映射map
public class Student {
// other code
@ElementCollection(targetClass = Float.class)
@CollectionTable(name = "subject_scores", joinColumns = @JoinColumn(name = "student_id", referencedColumnName = "id"))
@MapKeyClass(String.class)
@MapKeyColumn(name = "subject")
@Column(name = "score")
private Map<String, Float> scores; // K: name of subject. V: score for the subject
}
和list映射的区别是,多了MapKeyClass和MapKeyColumn,分别代表map的key所属的类和key在表中的属性名。@ElementCollection(targetClass = String.class)
表示value所属的类,@CollectionTable(name = "subject_scores")
这个name表示的value在表中的名字。
transaction.beginTransaction();
Student student = new Student();
// ...
Map<String, Float> subjects = new HashMap<>(2);
subjects.put("english", 90.0f);
subjects.put("math", 95.5f);
student.setScores(subjects);
sesssion.save(student);
transaction.commit();
数据库输出如下:
mysql> select * from subject_scores;
+------------+-------+---------+
| student_id | score | subject |
+------------+-------+---------+
| 9 | 90 | english |
| 9 | 95.5 | math |
+------------+-------+---------+
2 rows in set (0.00 sec)
mysql> desc subject_scores;
+------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| student_id | int(11) | NO | PRI | NULL | |
| score | float | YES | | NULL | |
| subject | varchar(255) | NO | PRI | NULL | |
+------------+--------------+------+-----+---------+-------+
3 rows in set (0.00 sec)
1.3.3 集合的性能
考虑这样一个情景:我们获取通过hibernate获取一个对象,该对象有个集合属性。集合包含了几万条数据。但在我们当前的业务当中,并不会访问该集合属性。如果hibernate在获取对象的时候,也从集合属性对应的表中取出所有元组并添加到集合里面,那hibernate的这种操作无疑是非常浪费内存的。幸运的是,hibernate的针对集合属性,默认采取延迟加载策略,只有在我们访问该集合属性的时候,才会从数据库中加载相关数据。如果我要改变默认的策略,我们可以为集合属性指定FetchType如下:
//...
@ElementCollection(targetClass = String.class, fetch = FetchType.EAGER)
private List<String> list;
延迟加载对应的属性是: FetchType.LAZY
1.4 集合元素是自定义的bean(不是int, String等)
这种情况下,我们把作为元素的自定义的bean用@Embeddable修饰,把该bean称作包含类的组合属性。其他的不变:
@Embeddable
public class BodyInfo {
private int height;
private int weight;
}
public class Student {
private BodyInfo bodyInfo;
// ...
}
// ...
BodyInfo bodyInfo = new BodyInfo();
bodyInfo.setHeight(178);
bodyInfo.setWeight(70);
student.setBodyInfo(bodyInfo);
Integer id = (Integer) session.save(student);
transaction.commit();
sql输出:
mysql> select * from student_info;
+----+-----+--------+--------+--------+
| id | age | name | height | weight |
+----+-----+--------+--------+--------+
| 9 | 18 | robert | 0 | 0 |
| 10 | 18 | robert | 178 | 70 |
+----+-----+--------+--------+--------+
6 rows in set (0.00 sec)
注意和1.1中的student_info输出对比,是不是多了2个列:height,weight。这两列也恰巧是自定义bean BodyInfo的两个属性。
这说明了hibernate自动把BodyInfo中的属性(必须是非集合)添加到了student_info表中。查看控制台:
Hibernate:
alter table hibernatedb.student_info
add column height integer not null
Hibernate:
alter table hibernatedb.student_info
add column weight integer not null
Hibernate:
insert
into
student_info
(age, height, weight, name)
values
(?, ?, ?, ?)
hibernate为student_info表增加了两个属性!!很智能吧。但需要注意的是,这种对数据库表结构的操作最好不要在生产环境中发生(可以删除配xml置文件中的hbm2ddl.auto的属性)。
如果BodyInfo中有集合属性怎么办?我们可以猜想,BodyInfo中的那些非集合属性仍然会添加到student_info表中,那些集合属性,会像前面一样,用单独的表来存储。这里就不验证了。
1.5 使用xml来配置映射
我们可以用xml来配置属性关系,从而不用注解。
比如如下:
src/com/example/test/Student.java
public class Student {
private Integer id;
private String name;
// getter and setters
}
src/com/example/test/Student.hbm.xml
<?xml version='1.0' encoding='utf-8'?>
<!--
~ Hibernate, Relational Persistence for Idiomatic Java
~
~ License: GNU Lesser General Public License (LGPL), version 2.1 or later.
~ See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
-->
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.example.test">
<class name="Student" table="student_info">
<id name="id" type="java.lang.Integer" column="id"/>
<property name="name" type="java.lang.String" column="name"/>
</class>
</hibernate-mapping>
src/hibernate.cfg.xml
<!-- some other settings -->
<session-factory>
<!--<mapping class="com.example.test.Student"/>-->
<mapping resource="com/example/test/Student.hbm.xml"/>
</session-factory>
现在用注解的比较多。