俗話說,自己寫的代碼,6個月后也是別人的代碼……復習!復習!復習!涉及的知識點總結如下:
- hibernate的主鍵生成策略
- UUID
- 配置的補充:hbm2ddl.auto屬性用法
- 注解還是配置文件
- hibernate注解的基本用法
- 使用Session API CRUD操作對象,以及對象狀態的轉換
- hibernate緩存的概念
- get()/load()的區別到底是什么,源碼分析
- 代理模式實現的懶加載
- saveOrUpdate()/merge()的區別
Assigned(常用,一般情況使用很方便):
由程序生成主鍵值,並且在save()之前指定,否則會拋出異常。
特點:主鍵的生成值完全由用戶決定,與底層數據庫無關。用戶需要維護主鍵值,在調用session.save()之前要指定主鍵值。唯一的例外:int auto_increment類型主鍵除外,在程序中不用指定主鍵值,直接看代碼:
sid是主鍵,且是int類型,設置為自動增長。

/** * StudentDao * * @author Wang Yishuai. * @date 2016/3/11 0011. * @Copyright(c) 2016 Wang Yishuai,USTC,SSE. */ public class StudentDao { private static final Logger LOG = LoggerFactory.getLogger(StudentDao.class); private Session session; @Before public void init() { Configuration configuration = new Configuration().configure(); SessionFactory sessionFactory = configuration.buildSessionFactory(); this.session = sessionFactory.getCurrentSession(); } @After public void clear() { } @Test public void save() { Transaction transaction = session.beginTransaction(); Student student = new Student(); try { //student.setSname("dashuai"); student.setSname("dadadad"); session.save(student); transaction.commit(); } catch (Exception e) { LOG.error("save error", e); transaction.rollback(); } } }
配置文件設置:

<hibernate-mapping> <!-- class 里面的屬性必須和對應的類名,表名保持一致,實現映射--> <class name="zhujian.vo.Student" table="students"> <!-- id代表數據庫表的主鍵, name="userId",對應數據庫表里的字段column="userId"--> <id name="sid" column="sid" type="string"> <generator class="assigned"/> </id> <!-- 數據庫里其他普通字段和實體屬性的映射,屬性的類型需要小寫--> <property name="sname" column="sname" type="string"/> </class> </hibernate-mapping>
先后保存兩個學生對象,沒有錯誤,如下:
即使我設置了主鍵生成策略為assigned,因為學生表的sid是自動增長且為int類型,所以程序中不用人工指定sid值,這是一個例外情況。其他情況如果設置主鍵為asigned策略,必須手動設主鍵值,否則會出問題。
把sid的自動增長設置取消,再運行之前的代碼,會發生問題:如果不設置sid(主鍵)值,則無法成功插入數據,且會生成主鍵為0的數據記錄(if之前沒有),本地測試沒有報錯,但是無法成功插入。
把主鍵改為String(varchar)類型(對象映射文件和實體類也需要修改),再運行之前的代碼,同樣只能插入sid為0的記錄(if之前沒有),其他的無法繼續插入。若改為手動設置主鍵值,則恢復正常。
Increment(MySQL不適用,集群不適用,多進程不適用,與底層數據庫有關)
Increment方式對主鍵值采取自動增長的方式生成新的主鍵值,但要求底層數據庫支持Sequence序列。如Oracle,DB2等。需要在映射文件xxx.hbm.xml中加入Increment標志符的設置。
特點:由Hibernate本身維護,適用於所有的數據庫,不適合多進程並發更新數據庫,適合單一進程訪問數據庫。不能用於群集環境。
比如現在有4個數據庫的集群,共享一張學生表,我從中查找某個編號(主鍵)為1000的學生,如果使用的hibernate的主鍵生成策略為increment,則無法保證編號1000的學生在集群里是唯一的。
Identity根據底層數據庫支持自動增長,無需手動設置主鍵,但不同的數據庫用不同的主鍵增長方式,移植性不好。
特點:與底層數據庫有關,適用於MySQL、DB2、MS SQL Server,采用數據庫生成的主鍵,用於為long、short、int類型生成唯一標識。使用SQL Server 和 MySQL 的自增字段,這個方法不能放到 Oracle 中,Oracle 不支持自增字段,要設定sequence(MySQL 和 SQL Server 中很常用),Identity無需Hibernate和用戶的干涉,使用較為方便,但不便於在不同的數據庫之間移植程序。
Sequence (常用)
Sequence需要底層數據庫支持Sequence方式,例如Oracle數據庫等。
特點:需要底層數據庫的支持序列,支持序列的數據庫有DB2、PostgreSql、Qracle、SAPDb等在不同數據庫之間移植程序,特別從支持序列的數據庫移植到不支持序列的數據庫需要修改配置文件。比如:Oracle:create sequence seq_name increment by 1 start with 1; 需要主鍵值時可以調用seq_name.nextval或者seq_name.curval得到,數據庫會幫助我們維護這個sequence序列,保證每次取到的值唯一,如:insert into tbl_name(id, name) values(seq_name.nextval, ‘Jimliu’);
配置:

<id name="id" column="id" type="long">
<generator class="sequence">
<param name="sequence">seq_name</param>
</generator>
</id>
Native(常用,推薦使用,移植性好,也適合多數據庫)
Native主鍵生成方式會根據不同的底層數據庫自動選擇Identity、Sequence、Hilo主鍵生成方式。
特點:根據不同的底層數據庫采用不同的主鍵生成方式。由於Hibernate會根據底層數據庫采用不同的映射方式,因此便於程序移植,項目中如果用到多個數據庫時,可以使用這種方式。
配置:

<id name="id" column="id">
<generator class="native" /></id>
UUID(常用在網絡環境,但是耗費存儲空間)
UUID使用128位UUID算法生成主鍵,能夠保證網絡環境下的主鍵唯一性,也就能夠保證在不同數據庫及不同服務器下主鍵的唯一性。
特點;能夠保證數據庫中的主鍵唯一性,生成的主鍵占用比較多的存貯空間。
配置:

<id name="id" column="id">
<generator class="uuid.hex" /></id>
Hilo(不常用)
使用高低位算法生成主鍵,高低位算法使用一個高位值和一個低位值,然后把算法得到的兩個值拼接起來作為數據庫中的唯一主鍵。Hilo方式需要額外的數據庫表和字段提供高位值來源。默認請況下使用的表是hibernate_unique_key,默認字段叫作next_hi。next_hi必須有一條記錄否則會出現錯誤。
特點:和底層數據庫無關,但是需要額外的數據庫表的支持,能保證同一個數據庫中主鍵的唯一性,但不能保證多個數據庫之間主鍵的唯一性。Hilo主鍵生成方式由Hibernate 維護,所以Hilo方式與底層數據庫無關,但不應該手動修改hilo算法使用的表的值,否則會引起主鍵重復的異常。
配置的補充:hbm2ddl.auto屬性用法
在項目的配置文件里設置:<property name="hbm2ddl.auto">update</property>,屬性含義的解釋:
- create:表示啟動的時候先drop數據庫,再create。
- create-drop: 也表示創建,只不過再系統關閉前執行一下drop。
- update: 這個操作啟動的時候會去檢查schema是否一致,如果不一致會做scheme更新。
- validate: 啟動時驗證現有schema與你配置的hibernate是否一致,如果不一致就拋出異常,並不做更新。
在Hibernate是用映射文件好還是用注解的方式好?
兩種方式本質上沒區別。使用元數據可以在寫實體的同時配好映射,而 XML 則需要來回切換配置和實體,不太方便。從維護上來說,注解更好。因為代碼和“配置”在一起,不容易漏掉信息。從這一點上來說,注解必須是第一選擇。但是也有人說到這樣一個問題,如果使用注解,有很大可能會違反開閉原則,使用配置文件則不會。閑言少敘,看代碼:

<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory name="MySQL"> <property name="connection.driver_class">com.mysql.jdbc.Driver</property> <property name="connection.url">jdbc:mysql://localhost:3306/bbs</property> <property name="connection.username">root</property> <property name="connection.password">123</property> <property name="dialect">org.hibernate.dialect.MySQL5Dialect</property> <property name="show_sql">true</property> <property name="format_sql">true</property> <property name="hbm2ddl.auto">update</property> <property name="current_session_context_class">thread</property> <mapping class="net.nw.vo.Students"/> </session-factory> </hibernate-configuration>
寫好配置文件,代碼如下:

package net.nw.vo; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; //學生實體類 @Entity public class Students { private int sid; private String sname; @Id @GeneratedValue(strategy=GenerationType.AUTO) public int getSid() { return sid; } public void setSid(int sid) { this.sid = sid; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } }
測試:

import net.nw.vo.Students; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.cfg.AnnotationConfiguration; public class HibernateTest { public static void main(String[] args) { AnnotationConfiguration annotationConfiguration = new AnnotationConfiguration().configure(); SessionFactory sessionFactory = annotationConfiguration.buildSessionFactory(); Session session = sessionFactory.getCurrentSession(); Transaction transaction = session.beginTransaction(); try { Students s = new Students(); //s.setSid(1); s.setSname("xxxx"); session.save(s); transaction.commit(); } catch (Exception ex) { transaction.rollback(); ex.printStackTrace(); } } }
執行成功!本地測試成功插入xxxxx(接之前的bbs數據庫表students):
小結:
- 使用AnnotationConfiguration創建config對象代替之前的Configuration方式(hibernate3的方式)。
- hibernate4中新增了ServiceRegistry接口。4.0以后改為使用ServiceRegistry 注冊:

Configuration cfg = new Configuration().configure(); ServiceRegistry serviceRegistry = new ServiceRegistryBuilder().applySettings(cfg.getProperties()).buildServiceRegistry(); SessionFactory factory = cfg.buildSessionFactory(serviceRegistry); Session s = factory.openSession();
- @Entity——必選的注解,在class類體上面,注解將一個類聲明為一個實體bean。屬性 name 可選,對應數據庫中的一個表。若表名與實體類名相同,則可以省略。
-
@Table(name="",catalog="",schema="") ——該注解可選,通常和@Entity 配合使用,只能標注在實體的 class 定義處,表示實體對應的數據庫表的信息。屬性:
-
name - 可選,表示表的名稱,默認地,表名和實體名稱一致,只有在不一致的情況下才需要指定表名
-
catalog - 可選,表示Catalog名稱,默認為 Catalog("").
-
schema - 可選 , 表示 Schema 名稱 , 默認為Schema("").
-
- @Id——必選,一般加在getter方法上面, 定義了映射到數據庫表的主鍵的屬性,一個實體只能有一個屬性被映射為主鍵,置於get方法前。
- @GeneratedValue(strategy=GenerationType,generator="") ——可選,用於定義主鍵生成策略。屬性:
- Strategy - 表示主鍵生成策略,取值有:
- GenerationType.AUTO - 根據底層數據庫自動選擇(默認),若數據庫支持自動增長類型,則為自動增長。
-
GenerationType.INDENTITY - 根據數據庫的Identity字段生成,支持DB2、MySQL、MS、SQL Server、SyBase與HyperanoicSQL數據庫的Identity類型主鍵。
-
GenerationType.SEQUENCE - 使用Sequence來決定主鍵的取值,適合Oracle、DB2等 ,支持Sequence的數據庫,一般結合@SequenceGenerator使用。(Oracle沒有自動增長類型,只能用Sequence)
-
GenerationType.TABLE - 使用指定表來決定主鍵取值,結合@TableGenerator使用。如:
@Id @TableGenerator(name="tab_cat_gen",allocationSize=1) @GeneratedValue(Strategy=GenerationType.Table)
-
Generator - 表示主鍵生成器的名稱,這個屬性通常和ORM框架相關 , 例如:Hibernate 可以指定 uuid 等主鍵生成方式
- Strategy - 表示主鍵生成策略,取值有:
-
@Version——可以在實體bean中使用@Version注解,通過這種方式可添加對樂觀鎖定的支持。
-
@Basic ——用於聲明屬性的存取策略:
-
@Basic(fetch=FetchType.EAGER) 即時獲取(默認的存取策略)
-
@Basic(fetch=FetchType.LAZY) 延遲獲取
-
-
@Temporal ——用於定義映射到數據庫的時間精度:@Temporal(TemporalType=TIME) 時間、@Temporal(TemporalType=DATE) 日期、@Temporal(TemporalType=TIMESTAMP) 兩者兼具
注解太多了,以后一一說明。
- 使用注解,在Hibernate.cfg.xml中同樣也需要注冊實體類。當然配置文件不用配,只是還是需要引入實體類的關系,<mapping class="net.nw.vo.Students"/>,配置文件方式的resource=改為了class=。
使用session api實現crud
結合之前的總結,看看crud時對象狀態的轉換關系。直接看代碼:
- create,session.save(obj);,注意這里面的玄機:
- 如果當前對象是持久態,則執行save方法,會自動觸發Update語句的執行,而不是insert語句。
- 如果當前對象為瞬時態,則執行save方法,會立刻執行insert語句,使得對象從瞬時態轉換為持久態。

@Test public void save() { Transaction transaction = session.beginTransaction(); Student student = new Student(); try { student.setSname("xiaoli"); // 之前的student是瞬時態的 session.save(student); // 之后變為持久態 transaction.commit(); } catch (Exception e) { LOG.error("save error", e); transaction.rollback(); } }
debug一下,驗證之前的理論:在sava處斷點,step over之后,立即控制台打印了insert into students(sname) values(?)。后台數據庫也插入了xiaoli。
下面看如果當前對象是持久態的情況:

@Test public void save() { Transaction transaction = session.beginTransaction(); // Student student = new Student(); try { // student.setSname("xiaoli"); // 從數據庫讀取xiaoli,該對象是持久態的 Student student = (Student) session.get(Student.class, 7); // 重新賦值,再保存 student.setSname("小李"); session.save(student); transaction.commit(); } catch (Exception e) { LOG.error("save error", e); transaction.rollback(); } }
還是在save處debug,發現首先發送SQL語句:

select
student0_.sid as sid2_0_,
student0_.sname as sname2_0_
from
students student0_
where
student0_.sid=?
執行save之后,發送的SQL是:

update students set sname=? where sid=?
道理很明顯,如果還是插入語句,則會插入新的記錄,顯然不對了。
- retrive,hibernate基本的查詢api是get和load(只根據主鍵查詢,不能根據其它字段查詢,如果想根據非主鍵查詢,可以使用HQL),之前也看了get的用法了,使用get查詢對象,執行之后,對象變為持久態的,立刻發送SQL語句select……而且也可以直接填寫實體類名,get查找返回的是真正的實體對象,如果找不到則返回null,代碼如下:

Student student = (Student) session.get("zhujian.vo.Student", 1);
LOG.info("sid = {}, sname = {}", student.getSid(), student.getSname());
debug一下:
執行48句之后:
再看load方法,參數寫法和get一樣的,但是load是一種懶加載的方式,只有使用的時候才生成sql語句:

@Test public void retrive() { Transaction transaction = session.beginTransaction(); try { Student student = (Student) session.load(Student.class, 2); student.setSname("小李"); session.save(student); transaction.commit(); } catch (Exception e) { LOG.error("save error", e); transaction.rollback(); } }
load方法查詢的對象是代理對象,且執行此方法時不會立即發出查詢語句。因為使用load加載對象去查詢某一條數據的時候並不會直接將這條數據以指定對象的形式來返回,而是在真正需要使用該對象里面的一些屬性的時候才會去查找對象。他的好處就是可以減少程序本身因為與數據庫頻繁的交互造成的處理速度緩慢的問題。
而hibernate的load方法延遲加載實現原理是使用代理(代理設計模式在hibernate的的應用),Hibernate的懶加載,是通過在內存中對類、集合等的增強(即在內存中擴展類的特性[繼承])來實現的,這些類通常稱為代理類。比如我們通過session.load(class, id)操作,加載一個對象的時候,hibernate返回的實際上是實體類的代理類實例。如圖:
執行65句之后:
但如通過session.get操作,則返回實際的對象實例(不是代理類實例),對上例而言,get操作返回實體類的實例。采用load()方法加載數據,如果數據庫中沒有相應的記錄,則會拋出異常:org.hibernate.ObjectNotFoundException: No row with the given identifier exists:……所以如果我知道該id在數據庫中一定有對應記錄存在,那么我就可以使用load方法來實現延遲加載。
session緩存(hibernate一級緩存)概念
在繼續探討這個問題之前,必須先提前總結下緩存的一些東西。
Hibernate緩存包括兩大類:Hibernate一級緩存和Hibernate二級緩存。
一級緩存就是Session級別的緩存,一個Session做了一個查詢操作,它會把這個操作的結果放在一級緩存中,如果短時間內這個session(一定要同一個session)又做了同一個操作,那么hibernate直接從一級緩存中拿,而不會再去連數據庫取數據。它是屬於事務范圍的緩存,Session對象的生命周期通常對應一個數據庫事務或者一個應用事務,一級緩存中,持久化類的每個實例都具有唯一的OID。這一級別的緩存由hibernate管理,無需主動干預即可工作。
二級緩存就是 SessionFactory 級別的緩存,查詢的時候會把查詢結果緩存到二級緩存中,如果同一個sessionFactory創建的某個session執行了相同的操作,hibernate就會從二級緩存中拿結果,而不會再去連接數據庫,因為SessionFactory對象的生命周期和應用程序的整個過程對應,因此Hibernate二級緩存是進程范圍或者集群范圍的緩存,有可能出現並發問題,因此需要采用適當的並發訪問策略,該策略為被緩存的數據提供了事務隔離級別,需要人工配置和更改,可以動態加載,卸載(二級緩存是一個可配置的插件,默認下SessionFactory不會啟用這個插件。)。而一級緩存不可以卸載。
當Hibernate根據ID訪問數據對象的時候,首先從Session一級緩存中查,查不到,如果配置了二級緩存,那么從二級緩存中查,如果都查不到,再查詢數據庫,把結果按照ID放入到緩存刪除、更新、增加數據的時候,同時更新緩存。

Student student = (Student) session.get(Student.class, 6); LOG.info("student sid = {}, student sname = {}", student.getSid(), student.getSname()); Student student1 = (Student) session.get(Student.class, 6); LOG.info("student1 sid = {}, student1 sname = {}", student1.getSid(), student1.getSname());
debug查看效果:執行完第一個get語句,立即發送了SQL語句,執行第二個一樣的get語句時,我發現並沒打印SQL語句,而是直接打印了log,只能說明,它第二次一樣的對象查詢是在緩存中查找到的。
網上有人說get不從緩存中查找,這是不對的,還有人說get不會去二級緩存中查找,其實也不對。或者嚴格的說必須指定哪個版本的hibernate再來討論這個問題,下面還會分析源碼來再次佐證(hibernate 3)。
再看load的方式:

Student student = (Student) session.load(Student.class, 6); LOG.info("sid = {}, sname = {}", student.getSid(), student.getSname()); Student student1 = (Student) session.load(Student.class, 6); LOG.info("student1 sid = {}, student1 sname = {}", student.getSid(), student.getSname());
也是第二次查找同一個對象的時候,沒有再生成SQL語句,直接打印的兩個log,只能說明它是在緩存中查詢了。load方法也是走緩存的。那么具體怎么走,下面看源碼級別的分析。
load和get的源碼分析:
先看get和load的api源碼,位於SessionImpl類,它是Session接口的一個實現類:
上面是load方法的,下面是get方法:
兩種方式類似,那么繼續看重載的load和get方法源碼:

public Object get(String entityName, Serializable id) throws HibernateException { LoadEvent event = new LoadEvent(id, entityName, false, this); boolean success = false; Object var5; try { this.fireLoad(event, LoadEventListener.GET); success = true; var5 = event.getResult(); } finally { this.afterOperation(success); } return var5; } public Object load(String entityName, Serializable id) throws HibernateException { LoadEvent event = new LoadEvent(id, entityName, false, this); boolean success = false; Object var5; try { this.fireLoad(event, LoadEventListener.LOAD); if(event.getResult() == null) { this.getFactory().getEntityNotFoundDelegate().handleEntityNotFound(entityName, id); } success = true; var5 = event.getResult(); } finally { this.afterOperation(success); } return var5; }
關鍵區別是this.fireLoad(event, LoadEventListener.LOAD);和this.fireLoad(event, LoadEventListener.GET);這兩句代碼,官方文檔說,使用fireLoad方法把加載事件event和事件加載的監聽器(一個接口)的方式組合,也就是這兩個方法觸發事件的方式不一樣。下面先看看這兩個代表不同觸發方式的常量:

LoadEventListener.LoadType GET = (new LoadEventListener.LoadType("GET")).setAllowNulls(true).setAllowProxyCreation(false).setCheckDeleted(true).setNakedEntityReturned(false); LoadEventListener.LoadType LOAD = (new LoadEventListener.LoadType("LOAD")).setAllowNulls(false).setAllowProxyCreation(true).setCheckDeleted(true).setNakedEntityReturned(false);
區別就是:在allowNulls上get允許空,也就是get找不到就返回null,而load不允許空,所以拋出異常!而在allowProxyCreation上get不允許代理創建,而load允許代理的創建,記住這點。繼續看fireload方法源碼:

private void fireLoad(LoadEvent event, LoadType loadType) { this.errorIfClosed(); this.checkTransactionSynchStatus(); LoadEventListener[] loadEventListener = this.listeners.getLoadEventListeners(); for(int i = 0; i < loadEventListener.length; ++i) { loadEventListener[i].onLoad(event, loadType); } }
使用的內部的LoadEventListener[] loadEventListener數組去調用loadEventListener[i].onLoad(event, loadType);方法,加載方式和事件作為參數傳入,下面看onload方法:

public void onLoad(LoadEvent event, LoadType loadType) throws HibernateException { EventSource source = event.getSession(); EntityPersister persister; if(event.getInstanceToLoad() != null) { persister = source.getEntityPersister((String)null, event.getInstanceToLoad()); event.setEntityClassName(event.getInstanceToLoad().getClass().getName()); } else { persister = source.getFactory().getEntityPersister(event.getEntityClassName()); } if(persister == null) { throw new HibernateException("Unable to locate persister: " + event.getEntityClassName()); } else { if(!persister.getIdentifierType().isComponentType() || EntityMode.DOM4J != event.getSession().getEntityMode()) { Class keyToLoad = persister.getIdentifierType().getReturnedClass(); if(keyToLoad != null && !keyToLoad.isInstance(event.getEntityId())) { throw new TypeMismatchException("Provided id of the wrong type for class " + persister.getEntityName() + ". Expected: " + keyToLoad + ", got " + event.getEntityId().getClass()); } } EntityKey keyToLoad1 = new EntityKey(event.getEntityId(), persister, source.getEntityMode()); try { if(loadType.isNakedEntityReturned()) { event.setResult(this.load(event, persister, keyToLoad1, loadType)); } else if(event.getLockMode() == LockMode.NONE) { event.setResult(this.proxyOrLoad(event, persister, keyToLoad1, loadType)); } else { event.setResult(this.lockAndLoad(event, persister, keyToLoad1, loadType, source)); } } catch (HibernateException var7) { log.info("Error performing load command", var7); throw var7; } } }
找到關鍵的代碼,因為我的測試例子里,兩個方法都沒有調用帶 lockMode 的 api,它們默認 null,執行這一句:
get和load查詢方法都走該句,那么繼續看proxyOrLoad方法:

protected Object proxyOrLoad(LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadType options) { if(log.isTraceEnabled()) { log.trace("loading entity: " + MessageHelper.infoString(persister, event.getEntityId(), event.getSession().getFactory())); } if(!persister.hasProxy()) { return this.load(event, persister, keyToLoad, options); } else { PersistenceContext persistenceContext = event.getSession().getPersistenceContext(); Object proxy = persistenceContext.getProxy(keyToLoad); return proxy != null?this.returnNarrowedProxy(event, persister, keyToLoad, options, persistenceContext, proxy):(options.isAllowProxyCreation()?this.createProxyIfNecessary(event, persister, keyToLoad, options, persistenceContext):this.load(event, persister, keyToLoad, options)); } }
關鍵語句 if(!persister.hasProxy()),大概機制就是當該方式沒有發現代理存在,也就是get的加載方式,就return this.load(event, persister, keyToLoad, options) 方法的值,那么看load方法的源碼:

protected Object load(LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadType options) { if(event.getInstanceToLoad() != null) { if(event.getSession().getPersistenceContext().getEntry(event.getInstanceToLoad()) != null) { throw new PersistentObjectException("attempted to load into an instance that was already associated with the session: " + MessageHelper.infoString(persister, event.getEntityId(), event.getSession().getFactory())); } persister.setIdentifier(event.getInstanceToLoad(), event.getEntityId(), event.getSession().getEntityMode()); } Object entity = this.doLoad(event, persister, keyToLoad, options); boolean isOptionalInstance = event.getInstanceToLoad() != null; if((!options.isAllowNulls() || isOptionalInstance) && entity == null) { event.getSession().getFactory().getEntityNotFoundDelegate().handleEntityNotFound(event.getEntityClassName(), event.getEntityId()); } if(isOptionalInstance && entity != event.getInstanceToLoad()) { throw new NonUniqueObjectException(event.getEntityId(), event.getEntityClassName()); } else { return entity; } }
關鍵一句:Object entity = this.doLoad(event, persister, keyToLoad, options);,到這里就一目了然了:

1 protected Object doLoad(LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadType options) { 2 if(log.isTraceEnabled()) { 3 log.trace("attempting to resolve: " + MessageHelper.infoString(persister, event.getEntityId(), event.getSession().getFactory())); 4 } 5 6 Object entity = this.loadFromSessionCache(event, keyToLoad, options);// 先從session緩存(一級緩存)中加載對象 7 8 if(entity == REMOVED_ENTITY_MARKER) { 9 log.debug("load request found matching entity in context, but it is scheduled for removal; returning null"); 10 return null; 11 } else if(entity == INCONSISTENT_RTN_CLASS_MARKER) { 12 log.debug("load request found matching entity in context, but the matched entity was of an inconsistent return type; returning null"); 13 return null; 14 } else if(entity != null) {// 在一級緩存里查找到對象就返回該對象 15 if(log.isTraceEnabled()) { 16 log.trace("resolved object in session cache: " + MessageHelper.infoString(persister, event.getEntityId(), event.getSession().getFactory())); 17 } 18 19 return entity; 20 } else { // 一級緩存找不到,get方法去二級緩存找 21 entity = this.loadFromSecondLevelCache(event, persister, options); 22 if(entity != null) {// 如果存在且找到就返回該對象 23 if(log.isTraceEnabled()) { 24 log.trace("resolved object in second-level cache: " + MessageHelper.infoString(persister, event.getEntityId(), event.getSession().getFactory())); 25 } 26 27 return entity; 28 } else { // 最后緩存找不到,才去數據庫查詢 29 if(log.isTraceEnabled()) { 30 log.trace("object not resolved in any cache: " + MessageHelper.infoString(persister, event.getEntityId(), event.getSession().getFactory())); 31 } 32 33 return this.loadFromDatasource(event, persister, keyToLoad, options); 34 } 35 } 36 }
顯然,get的查詢方式,首先查詢session緩存,也就是一級緩存,沒有就去查詢二級緩存,如果沒有二級緩存或者沒找到,最后才去查數據庫。
下面看看load方法,其實大同小異,和get的源碼差不多,只不過走了其他一些分支,還是看方法proxyOrLoad里,如果找到了代理(也就是使用的load查找方式),進入如下分支:

PersistenceContext persistenceContext = event.getSession().getPersistenceContext();// load方式先從事件的上下文環境中查找(一級緩存session) // 返回的對象賦值給proxy,名字都那么明顯了,代理對象 Object proxy = persistenceContext.getProxy(keyToLoad); // 如果是空的,就創建代理對象,不是空的,先判斷是否允許創建代理(還記得之前的LOAD和GET常量么,就在這里起作用),load肯定允許,那么開始創建代理 return proxy != null?this.returnNarrowedProxy(event, persister, keyToLoad, options, persistenceContext, proxy):(options.isAllowProxyCreation()?this.createProxyIfNecessary(event, persister, keyToLoad, options, persistenceContext):this.load(event, persister, keyToLoad, options));
到這里肯定明確一點:load查找方式首先會去緩存里查找,那么load的代理對象在獲取對象屬性時究竟如何加載數據?
如果緩存中找不到對象,則進入該方法:

// 該內部的私有方法中第一個log就說了:實體代理類先從session中查找 private Object returnNarrowedProxy(LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadType options, PersistenceContext persistenceContext, Object proxy) { log.trace("entity proxy found in session cache"); // 執行的是懶加載 LazyInitializer li = ((HibernateProxy)proxy).getHibernateLazyInitializer(); if(li.isUnwrap()) {// 沒有解包,就直接返回該代理對象 return li.getImplementation(); } else { Object impl = null; // load查找允許創建代理,那么這個if肯定不進入執行 if(!options.isAllowProxyCreation()) { impl = this.load(event, persister, keyToLoad, options); // 沒有找到就返回異常信息 if(impl == null) { event.getSession().getFactory().getEntityNotFoundDelegate().handleEntityNotFound(persister.getEntityName(), keyToLoad.getIdentifier()); } } // 重寫使用代理包裝一下再返回 return persistenceContext.narrowProxy(proxy, persister, keyToLoad, impl); } }
也就是說,此時的load方式直接返回相應的對象,不會發送SQL語句查詢數據庫,也不管給定的id是否在數據庫中存在數據對象。也就是此時此刻load不會直接訪問數據庫,只是簡單地返回一個由底層封裝的一個代理對象,永遠不為null,null會拋出異常。當然了,當然了最后load也會查數據,只不過是在實際使用數據時才去查詢二級緩存,二級緩存找不到,最后去數據庫查找,否則拋出異常。
以上源碼,來自hibernate 3。如果其他版本情況大同小異,大不了變化了就分析源碼,看官方文檔……再也不隨便相信網上的一些不負責任轉載的文章了。
Get()和Load()的區別小結
- get()方法默認不支持lazy(延遲加載)功能,而load支持延遲加載get方法,get方法首先查詢session緩存,也就是一級緩存,沒有找到查二級緩存,如果沒二級緩存或者找不到,則查詢數據庫,而load方法創建時首先查詢session緩存,沒有就創建代理對象並返回,實際使用數據時才查詢二級緩存,沒有就查詢數據庫。
- 如果找不到符合條件的記錄,get方法返回null,而load方法拋出異常(ObjectNotFoundException),load擁有不為null。
- 使用load方法,一般都假定你要取得對象肯定是存在的,才能使用,否則報錯,而get方法則嘗試查找,如果不存在,就返回null。
- get()和load()只根據主鍵查詢,不能根據其它字段查詢,如果想根據非主鍵查詢,可以使用HQL或者原生SQL。
- 使用load()時如果在session關閉之后再查詢此對象,會報異常:could not initialize proxy - no Session。處理辦法:在session關閉之前初始化一下查詢出來的對象:Hibernate.initialize(對象);
- 雖然好多書中都這么說:“get()永遠只返回實體類”,但實際上這是不正確的,get方法如果在session緩存中找到了該id對應的對象,如果剛好該對象前面是被代理過的,如被load方法使用過,或者被其他關聯對象延遲加載過,那么返回的還是原先的代理對象,而不是實體類對象,如果該代理對象還沒有加載實體數據(就是id以外的其他屬性數據),那么它會查詢二級緩存或者數據庫來加載數據,但是返回的還是代理對象,只不過已經加載了實體數據。
- 前面已經從源碼角度講了,get方法首先查詢session緩存,沒有的話查詢二級緩存,最后查詢數據庫;load方法創建時首先查詢session緩存,沒有就創建代理,實際使用數據時才查詢二級緩存和數據庫。
這里比較細致了,繼續看刪除和修改:
- update,總結兩點:
- 瞬時態不能執行update()。
- 持久態和游離態可以執行update()。
- 注意:代理對象在session關閉之后,不能執行update()。
第一個瞬時態對象,也就是數據庫沒有該對象保存,那么肯定更新不了啦!很好理解。看代碼:

try { Student student = new Student(); // 創建瞬時態對象student student.setSid(9); student.setSname("隔壁老王"); // 更新瞬時態對象 session.update(student); transaction.commit(); } catch (Exception e) { LOG.error("save error", e); transaction.rollback(); }
報錯:org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]
第二個持久態肯定能執行更新,毋庸置疑,先從數據庫中把對象讀取,變為持久態,在更新就ok。而游離態是存在於數據庫,但是脫離session管理的對象,也是可以更新的。道理都一樣。比如:

@Test public void updateObj() { Transaction transaction = session.beginTransaction(); try { Student student = (Student) session.get(Student.class, 1); transaction.commit(); // 讀取一個持久對象,什么都不做,提交事務,session自動關閉(使用的線程封閉的session) // 此時student變為游離態 // 重新開啟session和事務 session = sessionFactory.getCurrentSession(); Transaction transaction1 = session.beginTransaction(); session.update(student); // ok transaction1.commit();// ok } catch (Exception e) { LOG.error("save error", e); transaction.rollback(); } }
發送了SQL:
update
students
set
sname=?
where
sid=?
Process finished with exit code 0
- SaveOrUpdate():當對象從瞬時態轉換為持久態時,如果不確定主鍵是否沖突,推薦使用SaveOrUpdate()。很簡單,顧名思義,設置的主鍵在數據庫存在,自動執行的是更新方法update,如果主鍵不存在,那么執行的是save方法保存這個對象,把對象從瞬時態變為持久態。站在用戶角度,用戶事先不知道該主鍵在數據庫存在否,使用這個保險。當然了,主鍵生成策略不能是native,因為它會自增,根本不會沖突。使用assigned就可以。
- delete:持久態或者游離態對象都可以執行delete()方法。不過瞬時態對象也可以執行delete(),但Hibernate並不推薦使用delete()刪除瞬時狀態對象,因為這樣做沒有任何意義。
- merge方法:如果數據庫中有該記錄,則更新該記錄,如果不存在該記錄,則進行insert。乍一看很像saveOrUpdate方法,但是有區別,因為執行完merge(obj)后,它返回一個持久化對象的引用,而實參obj本身還是游離的。
merge和saveOrUpdate方法區別
使用saveOrUpdate,如果數據庫中有記錄,會無條件執行update,如果數據庫中無記錄,則執行insert操作。merge方法是把我們提供的對象變為游離狀態的對象,只不過merger重新返回一個新的持久化對象的引用。而saveOrUpdate則把我們提供的對象轉變為一個持久化對象。
也就是說saveOrUpdate后的對象會納入session的管理,對象的狀態會跟數據庫同步,再次查詢該對象會直接從session緩存中取,merge后的對象不會納入session的管理,再次查詢該對象還是會從數據庫中取。

Students s1 = new Students(); s1.setSid(4);// 假設id=4的對象存在數據庫 s1.setSname(“masi”);//s1是托管態的 Students s2 =(Students)session.merge(s1);
但是merge(s1)之后,返回的s2(引用)這個對象納入了session管理,和數據庫同步,變為持久化態,而s1還是托管態的,沒有session管理它。
歡迎關注
dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!