Hibernate
創建 hibernate 工程示例:
創建工程,引入 jar 包
創建配置文件 hibernate.cfg.xml, 配置數據庫連接
編寫實體類(entity), 標明注解, 然后配置在 hibernate.cfg.xml 中
創建?SessionFactory, 獲取?Session, 通過操作實體類操作數據庫。
對象的三種狀態:
Transient, 瞬時狀態,指的是對象已經被初始化,但沒有跟 hibernate 的 session 建立過聯系,即數據庫里沒有數據對應。
Persistent, 持久化狀態,指的是對象在數據中有對應數據,對象有 id 值。它可能是通過 save 或 load 等方式得到的,并且在 session 緩存中有定義。
Detached, 脫管狀態,曾經被持久,在數據庫中有數據對應。但是,在 session 緩存里沒有記錄。也許是 session 關閉了,也許是清空了。
狀態之間可以進行轉換,下面是大致的轉換流程:
get/load/query()
get/load 會優先在 session 緩存里尋找對象,如果找不到,再去查詢數據庫
query 會直接查詢數據庫
get 不懶,會立刻查詢。如果沒有找到,那么返回 null
load 延遲加載,立刻返回一個代理對象。如果沒有找到,那么拋出異常
LazyInitializationException !!!
flush/refresh()
flush 將 session 緩存里的數據同步到數據庫,觸發相應的 sql 語句。
以下情況,會觸發 flush 操作:
調用 commit 的時候,會觸發 session.flush() 操作。
執行 session.createQuery() 查詢的時候,也會觸發 flush 操作。
手動執行 flush 操作。
refresh 是將數據庫里的信息,同步到 session 緩存。
clear/evict()
從 session 緩存中清理數據
save/persist()
都是用來將瞬時對象變為持久化對象,即將數據插入數據庫,對應 insert 語句。
save 是 hibernate 原生的語法,persist 是 jpa 的語法。
在執行的時候,不會立刻插入數據,只有執行了 flush 操作,才真正觸發數據庫操作。
save/persist 方法會立刻為實體類對象生成主鍵。
他們的區別是, 如果在保存之前,重新手動賦予了主鍵:
save 會忽視你的賦值
persist 會拋異常
update/merge()
他們主要用來完成實體類對象的修改,對應的是 update 語句。
若更新一個持久化對象,可以不顯式調用 update, 因為 flush 操作會觸發 update
可以將一個脫管對象轉換為持久化對象
merge 是 jpa 中的語法
doWork
可以將 jdbc 的 connection 對象暴露出來,用于插入一些 jdbc 操作語法。
Identifier
-- JPA 默認
@GeneratedValue(strategy = GenerationType.AUTO/IDENTITY/SEQUENCE/TABLE)
-- JPA 定制序列/Table
@GeneratedValue(generator = "xxx")
@SequenceGenerator(name = "xxx", sequenceName = "seq_xxx", associateSize = 1)
@TableGenerator(name = "xxx", table = "tb_xxx")
-- Hibernate 格式的 generator:
@GeneratedValue(generator = "yyy")
@GenericGenerator(name = "yyy", strategy = "native")
@GenericGenerator(name = "yyy", strategy = "uuid2")
@GenericGenerator(name = "yyy", strategy = "table")
Association
1-N
一對多的關系,在數據庫的角度,需要使用外鍵維護這種關系。
一般情況下,在多的一邊的表上,建立一個外鍵映射到另一個表。
比如,有兩個表 author, book 一般而言,book 的定義類似是這樣的:
create table book {
? ? bookid int primary key,
? ? name varchar2(20) not null,
? ? price float,
? ? publish_date date default sysdate,
? ? -- 下面字段用來維護跟作者的關系
? ? -- 它是一個外鍵約束
? ? authorid references author
}
book/author 分別對應實體類 Book/Author,我們可以在其中任意一個實體類中,設置他們的關系。
如果只是在其中一個中設置關系,那么叫“單邊關系”、“單向關聯”,否則是“雙向關聯”。
其中最常用的是 多對一的單向關聯 和 *多對一的雙向關聯*。
多對一的單向:
public class Author {
? ? @Id @GeneratedValue private long id;
? ? private String name;
}
public class Book {
? ? @Id @GeneratedValue private long id;
? ? private String name;
? ? private FLoat price;
? ? // 只是在多的一段設置關系。這是非常常用的一種方式。
? ? // 用 @JoinColumn 定制外鍵字段的名字
? ? @ManyToOne @JoinColumn
? ? private Author author;
}
多對一的雙向關系:
// 多的一端,即主端,需要負責維護關系
public class Book {
? ? @Id @GeneratedValue private long id;
? ? private String name;
? ? private FLoat price;
? ? // 只是在多的一端設置關系。這是非常常用的一種方式。
? ? // 用 @JoinColumn 定制外鍵字段的名字
? ? @ManyToOne @JoinColumn
? ? private Author author;
}
// 一的一端,即從端,需要當甩手掌柜
public class Author {
? ? @Id @GeneratedValue private long id;
? ? private String name;
? ? // 不要讓雙方都去維護關系,不然會有沖突或重復。
? ? // 一般情況下,需要讓多的一端維護關系即可。這里用 mappedBy 表名,自己當甩手掌柜。
? ? @OneToMany(mappedBy = "author")
? ? private Set<Books> books = new HashSet<>();
}
在數據插入的時候,要先保存一的一端,再保存多的一端,否則,會有冗余的 SQL 語句。
M-N
多對多的關系,需要使用中間表維護雙方關系。對應的注解為 @ManyToMany
必須為雙方制定從屬關系,也就是將維護關系的責任交給其中一個實體類(mappedBy),從而避免重復或沖突。
可以使用 @JoinTable 對中間表進行定制
例子:
@Entity
public class Emp {
? ? @ManyToMany? // 負責關系的維護
? ? @JoinTable(...)
? ? private Set<Project> projects = new HashSet<>();
}
@Entity
public class Project {
? ? @ManyToMany(mappedBy = "projects")? // 甩手掌柜
? ? private Set<Emp> emps = new HashSet<>();
}
1-1
兩種方式:
在其中一個表上創建一個列,保存另一個表的主鍵。即外鍵關聯。
兩個表,有關聯的數據,使用相同的主鍵。即主鍵關聯。
外鍵關聯:
@Entity
public class Person {
? ? @Id @GeneratedValue? // 主鍵自動生成
? ? private long id;
? ? @OneToOne @JoinColumn? // 負責維護外鍵
? ? private IdCard idcard;
}
@Entity
public class IdCard {
? ? @Id @GeneratedValue // 主鍵自動生成
? ? private long id;
? ? @OneToOne(mappedBy="idcard")? // 甩手掌柜
? ? private Person person;
}
主鍵關聯:
@Entity
public class Person {
? ? @Id? ? ? // 主鍵*不要*自動生成!!
? ? private long id;
? ? @OneToOne // 負責維護外鍵,將外鍵映射到主鍵。即將另一張表的外鍵映射到本表的主鍵。
? ? @MapsId @JoinColumn(name = "id")
? ? private IdCard idcard;
}
@Entity
public class IdCard {
? ? @Id @GeneratedValue // 主鍵自動生成
? ? private long id;
? ? @OneToOne(mappedBy="idcard")? // 甩手掌柜
? ? private Person person;
}
Embed
這不屬于關聯關系,只是一種包含:
@Entity
class Person {
? ? @Embedded
? ? private Name name;
}
@Embeddable
class Name {
? ? String firstName;
? ? String lastName;
}
Inheritance
SINGLE_TABLE
將所有的東西塞進 一張表 中,即所有的子類跟父類使用一張表, 在這張表中使用“區別列”(DiscriminatorColumn)來區分各個類。
這是默認的繼承策略。
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "xxx") // 可以定制分割列的名字
public class Animal {}
@Entity
@DiscriminatorValue("狗") // 可以定制
public class Dog extend Animal {}
它并不符合范式,但也有自己的優點:
使用了區別的列
只使用了一張表,所以查詢速度快
缺點:子類的獨有列,不能添加唯一/非空約束
缺點:太多冗余字段
JOINED
是一種完全“符合范式”的設計:
將所有共有的屬性提取到父表中
僅將子類特有的屬性保存到子表中
父表跟子表通過外鍵的方式建立關系
如果查詢子表的詳細數據,通過關聯查詢關聯相關表即可
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Animal { }
@PrimaryKeyJoinColumn(name = "xxxxid")? // 可以定制關聯主鍵
public class Dog extend Animal { }
總結:
優點:沒有任何冗余
缺點:查詢的效率低,因為需要關聯各張表
TABLE_PER_CLASS(union)
每個類對應一張表,大家互相隔離,各自為政!
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Animal { }
@Entity
public class Dog extend Animal { }
總結:
優點:獨立,自由,查詢快
如果只查詢子類,那么不需要任何關聯;但如果查詢父類的話,需要使用 Union 關聯各表
缺點:存在冗余字段
缺點:如果要更新父類中的字段,每個子表都需要去更新
MappedSuperclass
如果父類不是 Entity,只是為子類提供公共屬性,那么,將其注解為 @MappedSuperclass 即可。
@MappedSuperclass
abstract public class Person {
? ? @Id private long id;
? ? @Column private String name;
}
@Entity
public class Girl extend Person {
? ? private String wechat;
}
@Entity
public class Boy extend Person {
? ? private String address;
}
**
級聯(Cascade)
比如說,一個部門有很多員工,它們是多對一的關系。如果我們要刪除1號部門:
Dept d = session.load(Dept.class, 1L);
session.delete(d);
我們會刪除失敗并得到一個異常,因為部門被員工數據引用,所以要刪除部門前,需要先將引用到部門的所有員工刪掉。
如果我們不想手動刪除部門內部員工,那么可以采取 *級聯操作*,即對 Dept 實體類中的 emps 屬性這樣設置:
@OneToMany(mappedBy="dept", cascade=CascadeType.REMOVE)
private List<Emp> emps = new ArrayList<>();
那么,再去執行刪除操作的時候,部門、連帶它所有的員工,都會被刪除。一步到位,快速絕倫。
除了刪除操作,級聯的類型還有:
CascadeType.PERSIST
CascadeType.MERGE
CascadeType.REFRESH
CascadeType.ALL (快捷方式,代指所有)
雖然 cascade 會讓我們的代碼更簡介,使用更方便。但是,在工業環境中,*不建議使用 cascade 設置*。
刪除數據的方式
第一種方法:
// 優點:快速簡潔
// 缺點:不能關聯刪除
Product product = new Product();
product.setId(44L);
session.delete(product);
第二種方法:
// 優點,能關聯刪除
// 缺點,不直接
Product product = session.load(Product.class, id); // load, not get
session.delete(product);
第三種方法:
// 優點,更靈活
// 缺點,跟第一種方式一樣,不能刪除關聯
int result = session
? ? .createQuery("delete Product where id = :id")
? ? .setParameter("id", 44L)
? ? .executeUpdate();
查詢
get/load
根據主鍵進行查詢。這是最基本,最高效的一種查詢手段。
Query
//// 基本語法
String hql = "from xxx where yyy";
Query query = session.createQuery(hql);
query.setParameter("aaa", "bbb");
query.uniqueResult();
// 可以用鏈式語法簡化語句
session.createQuery("from xxx where yyy").setParameter("aaa", "bbb").uniqueResult();
//// select 語句 和 返回值
from Emp e where e.name = 'x';? ? ? ? ? // 默認不需要寫 select, 那么會將結果封裝到 Emp 對象中
select e from Emp e where e.name = 'x';? // 上面的語句,跟此句是一致的
select name from Emp;? ? ? ? ? ? ? ? ? ? // 返回值:Object
select name, salary from Emp;? ? ? ? ? ? // 返回值:Object[]
select new list(name) from Emp;? ? ? ? ? // 返回值:ArrayList
select new map(name, salary) from Emp;? // 返回值:HashMap
select new map(name as name, salary as sal) from Emp; // 定制 key 值
select new Boy(name, salary) from Emp;? // 返回值:Boy 對象
//// 得到返回結果
session.createQuery("from Book", Book.class).uniqueResult();
session.createQuery("from Book", Book.class).list();
session.createQuery("from Book", Book.class).iterate().next();
// 過濾操作
session.createFilter(customer.getOrders(), "where price > 5000").list();
//// 聚合函數及其他運算符的使用
// 返回值:Object[]
select max(salary), avg(salary), sum(salary) from Emp;
// group by
select max(salary), avg(salary), sum(salary) from Emp e group by e.department;
// 將結果封裝到 map 中
select new map(max(salary) as maxsal, avg(salary) as avgsal, sum(salary) as sumsal) from Emp e group by e.department;
// 運算符和函數
select sum(salary + nvl(commission, 0)) as res from Emp;
//// join
// Query 不能使用 JOIN 抓取策略。Query 默認使用 select 語句進行關聯數據的加載。
// 如果想強制使用 join 語句,需要通過 hql 語句指定:
/// 1. 隱式設置
from Emp e where e.department.location = 'NEW YORK';
/// 2. 顯式調用
// fetch 決定最后結果的形式:
//? - 有 fetch: [Emp, Emp, ...]
//? - 無 fetch: [[Emp, Dept], [Emp, Dept]]
from Emp e join e.dept where e.name = 'xxx';
from Emp e left join e.dept where e.name = 'xxx';
from Emp e left join fetch e.dept where e.name = 'xxx';
//// 分頁、總行數
long count = session.createQuery("select count(*) from Emp", Long.class).uniqueResult();
long count = session.createQuery("select count(*) from Emp", Long.class).iterate().next();
// oracle: rownum/row_number()
// sqlserver: top/row_number()
// mysql/sqlite: limit x offset y
// hibernate 通過下面語句屏蔽了底層細節:
/// 從 80 行開始,取 5 行記錄
session.createQuery("from xxx").setFirstResult(80).setMaxResults(5).list();
//// delete & update
delete Emp where name = :oldName;
update Emp set name = :newName where id = :id;
// 級聯操作的設置,對 Query 也是無效的,比如,想刪除一個部門,需要先刪除員工,再刪部門:
delete Emp e where e.department.deptno = '#DN';
delete Dept where deptno = '#DN';
Criteria
Criteria,標準、規范,它是 Criterion 的復數形式。
優勢:
面向對象
不用拼接sql,方便擴展
統一性,跨數據庫
Criteria 接口: 表示特定類的一個查詢
Criterion 接口: 表示一個限定條件
示例:
// Session 是 Criteria 的工廠
// Criterion 的主要實現由 Example、Junction 和 SimpleExpression
// Criterion 一般通過 Restrictions 提供的工廠方法獲得
List<Emp> emps = session.createCriteria(Emp.class) // 創建
? ? .add( Restrictions.like("name", "K%") )? ? ? ? // 模糊
? ? .add( Restrictions.gt( "salary", 2000F ) )? ? // 大于
? ? .addOrder( Order.desc("salary") )? ? ? ? ? ? ? // 排序-1
? ? .addOrder( Order.desc("commission") )? ? ? ? ? // 排序-2
? ? .list();
// 約束可以按邏輯分組
List<Emp> emps = sess.createCriteria(Emp.class)
? ? .add( Restrictions.like("name", "K%") )
? ? .add( Restrictions.or( Restrictions.ge( "salary", 3000F ),
? ? ? ? ? ? ? ? ? ? ? ? ? Restrictions.isNotNull("commission") ) )
? ? .list();
// Property~Example 是添加約束的另兩種方法
List<Emp> emps = session.createCriteria(Emp.class)
? ? .add(Property.forName("name").eq("KING")) // Property
? ? .add(Example.create(king))? ? // 將 king 上的數據封裝成條件
? ? .list();
//// 關聯查詢
List<Emp> emps = session.createCriteria(Emp.class)
? ? .createCriteria("depts")? ? ? // vs. createAlias
? ? .add( Restrictions.eq("location", "NEW YORK") )
? ? .list();
//// Projections 提供投影查詢,并能分組聚合
// 投影條件
ProjectionList projectionList = Projections.projectionList()
? ? .add( Projections.property("dept") )
? ? .add( Projections.rowCount() )
? ? .add( Projections.max("salary") )
? ? .add( Projections.sum("salary", "sum" ) )
? ? .add( Projections.groupProperty("dept") );
// 查詢結果
List<Object[]> rs = session.createCriteria(Emp.class)
? ? .setProjection( projectionList )
? ? .addOrder( Order.asc("sum") )
? ? .list();
NativeSQL
基本語法,默認的返回的結果為 Object[]:
session.createNativeQuery("select ename, sal from emp").list();
session.createNativeQuery("select * from emp").list();
session.createNativeQuery("select * from emp e, dept d where e.deptno=d.deptno and d.loc=:loc")
? ? .setParameter("loc", "NEW YORK")
? ? .list();
可以通過 addScalar() 設置返回類型,并限定結果:
// 下面的查詢,得到的結果為 Object[], 包含兩個元素:0:id / 1:name
session.createNativeQuery("select * from emp where id=9999")
? ? .addScalar("empno", StandardBasicType.INTEGER)
? ? .addScalar("ename", StandardBasicType.STRING)
? ? .list();
也可以將結果封裝到 Entity(實體類) 中:
// simplest
session.createNativeQuery("select * from emp where sal > 2000")
? ? .addEntity(Emp.class).list();
// with alias
session.createNativeQuery("select e.* from emp e where sal > 2000")
? ? .addEntity("e", Emp.class)
? ? .list();
// multiple
session.createNativeQuery("select e.*, d.* from emp e join dept d using (deptno) where e.sal > 2000")
? ? .addEntity("e", Emp.class)
? ? .addEntity("d", Dept.class)
? ? .list();
將結果封裝到普通對象(非實體類)。注意,必須要使用 addScalar() 設置字段:
List<Person> persons = session.createSQLQuery("select * from emp")
? .addScalar("ename", StandardBasicType.INTEGER)
? .addScalar("salary", StandardBasicType.FLOAT)
? .setResultTransformer(Transforms.aliasToBean(Person.class))
? .list();
NameQuery
Query Strategy
一個實體類對象,里面有各個屬性,這些屬性的值可能不是在同一張表中。
為了效率,需要有一定加載策略,主要兩個方面:
when,屬性數據的加載時機,是否在加載這個實體類的時候就立刻加載。
how,通過什么樣的語句加載,select/join/其他。
比如,有一個實體類,叫 Girl:
@Entity
public class Girl {
? ? // 基本數據,保存在 girl 表中的數據:
? ? //? select id, name from girl;
? ? // 這種數據的默認加載機制是:
? ? //? 1. when: 立刻加載(EAGER)
? ? //? 2. how:? SELECT 語句
? ? @Id private long id;
? ? private String name;
? ? // 關聯數據,單結果,保存在 boy 表中的:
? ? //? select * from boy where id='我的老父親,您的編號';
? ? // 這種方式的默認加載機制是:
? ? //? 1. when: 立刻加載(EAGER)
? ? //? 2. how:? LEFT JOIN 連接
? ? @ManyToOne
? ? private Boy father;
? ? // 關聯數據,結果集,保存在 bag 表中的
? ? //? select * from bag where big_owner='女孩的編號';
? ? // 這種屬性數據的默認加載機制是:
? ? //? 1. when: 延遲加載(LAZY)
? ? //? 2. how:? SELECT 語句
? ? @OneToMany(mappedBy = "girl")
? ? private Set<Bag> bags = new HashSet();
}
如果我們調用 session.load(Girl.class, 1L), 會加載編號為 1 的女孩的數據。
她的數據分為三種:
基本數據,包含在 girl 表中的,比如 =id/name=。
關聯數據/XtoOne,比如 father 屬性。
關聯數據/XtoMany,比如 bags 屬性。
可以通過 fetch 屬性/@Fetch 注解 定制加載策略,分別對應 when/how, 例:
@ManyToOne(fetch = FetchType.EAGER)// 定義加載的時機(when)
@Fetch(FetchMode.SELECT)? ? ? ? ? // 定義加載語句的樣式(how)
private Boy boyfriend;
如果 when 為 EAGER=,默認的 how 為 =FetchMode.JOIN
如果 when 為 EAGER=,可以定制使 how 為 =FetchMode.SELECT/SUBSELECT
如果 how 為 JOIN, 那么 when 只能是 EAGER
如果設置了 hibernate.default_batch_fetch_size 或在實體類/集合上標注了 @BatchSize, 會對 LAZY 屬性加載采取批量優化。
@Fetch(FetchMode.SUBSELECT) 可以優化 HQL 返回的列表的關聯數據查詢語句
*JOIN 策略對 Query 查詢無效*,如需關聯查詢,在語句中顯式調用 join 語句!
N+1 問題
比如,如果:
打印出編號大于10的部門中的所有員工姓名。
那么,語句大致如此:
String hql = "from Dept where depto > :dn";
List<Dept> depts = session.createQuery(hql, Dept.class)
? ? .setParameter("dn", 10)
? ? .list();
for(Dept dept: depts) {
? ? for(Emp emp : dept.getEmps()) {
? ? ? ? System.out.printf("部門: %s, 姓名: %s\n",
? ? ? ? ? ? ? ? ? ? ? ? ? dept.getName(),
? ? ? ? ? ? ? ? ? ? ? ? ? emp.getName());
? ? }
}
因為 @OneToMany 默認是 Lazy + SELECT 策略,所以,每個部門的員工只有使用的時候才去查詢。
這就導致了上面的語句發送很多條 select 語句(N+1),嚴重影響效率。
*這就是 N+1 問題*。
解決方案有主要有下面幾種:
在 hql 語句中,使用 join 語句進行關聯查詢。
將 Dept#emps 的策略設置為 SUBSELECT 方式。
采取批量抓取的優化方式(BatchSize),即在 Dept#emps 上面加上注解: =@BatchSize(size=n)=。
使用二級緩存。
緩存(Cache)
緩存分為三種:事務范圍;應用范圍;集群范圍。
二級緩存是應用范圍的緩存機制。適合放入二級緩存的數據:
很少修改,不會修改,或不允許被更改的數據(常量數據)
不是很重要,允許偶爾出錯的數據
而一些重要的數據或者修改頻繁的數據,是不適合放到緩存里的。
配置使用二級緩存過程:
加入 JAR 包支持:
"org.hibernate:hibernate-ehcache:5.2.11.Final"
配置 /ehcache.xml [可選]
在 hibernate.cfg.xml 中啟用:
<prop key="hibernate.cache.use_second_level_cache">true</prop>
<prop key="hibernate.cache.use_query_cache">true</prop>
<prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory</prop>
配置要被緩存的類或集合
@Cachable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
使用示例
session.createQuery("from Employee where id=7782").setCacheable(true).list();
session.createQuery("from Employee where id=7782").setCacheable(true).list();
session.createQuery("from Employee where id=7782").setCacheable(true).list();
鎖(Lock)
Hibernate 中,設置鎖定有下面三種方式:
session.load(Male.class, 1L, LockMode.WRITE)
session.lock(m, LockModeType.WRITE);
session.createQuery(hql).setLockMode(LockModeType.PESSIMISTIC_WRITE);
Hibernate 中鎖的類型,分為兩種:
悲觀鎖。使用數據庫底層的 for update 語句。數據會被鎖定,直到事務結束。
樂觀鎖。使用實體類中的額外字段( @Version )。它不會真正在數據上加鎖,而是用版本號區別記錄的不同。
-- 它會在初次讀取數據時將 version 一起讀出,得到【版本號】,比如 10
-- 等到提交數據的時候,發送下面語句:
update xxx set version = 10 + 1, ... where id = 2 and version = 10;
-- 如果數據被別人修改過,那么 version 已經不是 10,所以上面語句不會更新到任何數據。
-- 同樣,hibernate 會拋出下面異常:
---- javax.persistence.OptimisticLockException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
-- 從而防止了數據的修改沖突。
悲觀鎖更適用于修改頻率大,讀取不多的數據。樂觀鎖適用于修改非常少,但讀取特別多的數據。悲觀鎖需要耗費更多資源。