Hibernate中會(huì)經(jīng)常用到set等集合來(lái)表示1-N的關(guān)系。比如,我有Customer和Order兩個(gè)對(duì)象。其中,在Customer中有一個(gè)Order的set集合,表示在一個(gè)顧客可以擁有多個(gè)Order,而在Order對(duì)象中存在了一個(gè)Customer的對(duì)象,表示這個(gè)Order是哪個(gè)顧客下的單。這個(gè)算是比較典型的雙向1-N關(guān)聯(lián)。
這給我們帶來(lái)了很大的好處,當(dāng)我得到了Customer對(duì)象的時(shí)候,我們可以很方便的將與其相關(guān)聯(lián)的Order集合查詢出來(lái),這也非常符合我們的實(shí)際業(yè)務(wù),畢竟我們不可能給這個(gè)Cutomer對(duì)象別人的Order吧,這既不安全,而且對(duì)Customer的普通顧客來(lái)說(shuō),并無(wú)卵用。所以我們不得不說(shuō)Hibernate的ORM做的很好,但凡事都有但是(要是沒(méi)有但是,也就沒(méi)有寫(xiě)這篇文章的必要了)。
我們?cè)賹?duì)數(shù)據(jù)庫(kù)進(jìn)行訪問(wèn)的時(shí)候必須要考慮性能問(wèn)題(通俗點(diǎn)講,就是用少發(fā)SQL語(yǔ)句),當(dāng)我們?cè)O(shè)定了1-N這種關(guān)系后,查詢過(guò)程中就有可能出現(xiàn)N+1問(wèn)題。
關(guān)于N+1問(wèn)題,并不是本文的重點(diǎn)。但關(guān)于N+1問(wèn)題,我們需要知道的是,這個(gè)問(wèn)題會(huì)導(dǎo)致SQL語(yǔ)句的增加,也就是要與數(shù)據(jù)庫(kù)進(jìn)行更多的交互,這無(wú)疑會(huì)給項(xiàng)目以及后臺(tái)數(shù)據(jù)庫(kù)帶來(lái)影響。
Hibernate是一個(gè)持久化框架,經(jīng)常需要訪問(wèn)數(shù)據(jù)庫(kù)。如果我們能夠降低應(yīng)用程序?qū)ξ锢頂?shù)據(jù)庫(kù)訪問(wèn)的頻次,那會(huì)提供應(yīng)用程序的運(yùn)行性能。緩存內(nèi)的數(shù)據(jù)是對(duì)物理數(shù)據(jù)源中的數(shù)據(jù)的復(fù)制,應(yīng)用程序運(yùn)行時(shí)先從緩存中讀寫(xiě)數(shù)據(jù)。
緩存就是數(shù)據(jù)庫(kù)數(shù)據(jù)在內(nèi)存中的臨時(shí)容器,包括數(shù)據(jù)庫(kù)數(shù)據(jù)在內(nèi)存中的臨時(shí)拷貝,它位于數(shù)據(jù)庫(kù)與數(shù)據(jù)庫(kù)訪問(wèn)層中間。ORM在查詢數(shù)據(jù)時(shí)首先會(huì)根據(jù)自身的緩存管理策略,在緩存中查找相關(guān)數(shù)據(jù),如發(fā)現(xiàn)所需的數(shù)據(jù),則直接將此數(shù)據(jù)作為結(jié)果加以利用,從而避免了數(shù)據(jù)庫(kù)調(diào)用性能的開(kāi)銷。而相對(duì)內(nèi)存操作而言,數(shù)據(jù)庫(kù)調(diào)用是一個(gè)代價(jià)高昂的過(guò)程。
Hibernate緩存包括兩大類:一級(jí)緩存和二級(jí)緩存。
那么什么樣的數(shù)據(jù)適合放入到緩存中?
什么樣的數(shù)據(jù)不適合放入到緩存中?
首先看一個(gè)非常簡(jiǎn)單的例子:
@Test
public void test() {
Customer customer1 = (Customer) session.load(Customer.class, 1);
System.out.println(customer1.getCustomerName());
Customer customer2 = (Customer) session.load(Customer.class, 1);
System.out.println(customer2.getCustomerName());
}
看一下控制臺(tái)的輸出:
Hibernate:
select
customer0_.CUSTOMER_ID as CUSTOMER1_0_0_,
customer0_.CUSTOMER_NAME as CUSTOMER2_0_0_
from
CUSTOMERS customer0_
where
customer0_.CUSTOMER_ID=?
Customer1
Customer1
我們可以看到,雖然我們調(diào)用了兩次session的load方法,但實(shí)際上只發(fā)送了一條SQL語(yǔ)句。我們第一次調(diào)用load方法時(shí)候,得到了查詢結(jié)果,然后將結(jié)果放到了session的一級(jí)緩存中。此時(shí),當(dāng)我們?cè)俅握{(diào)用load方法,會(huì)首先去看緩存中是否存在該對(duì)象,如果存在,則直接從緩存中取出,就不會(huì)在發(fā)送SQL語(yǔ)句了。
但是,我們看一下下面這個(gè)例子:
@Test
public void test() {
Customer customer1 = (Customer) session.load(Customer.class, 1);
System.out.println(customer1.getCustomerName());
transaction.commit();
session.close();
session = sessionFactory.openSession();
transaction = session.beginTransaction();
Customer customer2 = (Customer) session.load(Customer.class, 1);
System.out.println(customer2.getCustomerName());
}
我們解釋一下上面的代碼,在第5、6、7、8行,我們是先將session關(guān)閉,然后又重新打開(kāi)了新的session,這個(gè)時(shí)候,我們?cè)倏匆幌驴刂婆_(tái)的輸出結(jié)果:
Hibernate:
select
customer0_.CUSTOMER_ID as CUSTOMER1_0_0_,
customer0_.CUSTOMER_NAME as CUSTOMER2_0_0_
from
CUSTOMERS customer0_
where
customer0_.CUSTOMER_ID=?
Customer1
Hibernate:
select
customer0_.CUSTOMER_ID as CUSTOMER1_0_0_,
customer0_.CUSTOMER_NAME as CUSTOMER2_0_0_
from
CUSTOMERS customer0_
where
customer0_.CUSTOMER_ID=?
Customer1
我們可以看到,發(fā)送了兩條SQL語(yǔ)句。其原因是:Hibernate一級(jí)緩存是session級(jí)別的,所以如果session關(guān)閉后,緩存就沒(méi)了,當(dāng)我們?cè)俅未蜷_(kāi)session的時(shí)候,緩存中是沒(méi)有了之前查詢的對(duì)象的,所以會(huì)再次發(fā)送SQL語(yǔ)句。
我們稍微對(duì)一級(jí)緩存的知識(shí)點(diǎn)進(jìn)行總結(jié)一下,然后再開(kāi)始討論關(guān)于二級(jí)緩存的內(nèi)容。
Session的緩存有三大作用:
@Test
public void test() {
List<Customer> customers = session.createQuery("select c.customerName from Customer c").list();
System.out.println(customers.size());
Customer customer2 = (Customer) session.load(Customer.class, 1);
System.out.println(customer2.getCustomerName());
}
我們首先是只取出Customer的name屬性,然后又嘗試著去Load一個(gè)Customer對(duì)象,看一下控制臺(tái)的輸出:
Hibernate:
select
customer0_.CUSTOMER_NAME as col_0_0_
from
CUSTOMERS customer0_
3
Hibernate:
select
customer0_.CUSTOMER_ID as CUSTOMER1_0_0_,
customer0_.CUSTOMER_NAME as CUSTOMER2_0_0_
from
CUSTOMERS customer0_
where
customer0_.CUSTOMER_ID=?
Customer1
這一點(diǎn)其實(shí)很好理解,我本身就沒(méi)有查處Customer的所有屬性,那我又怎么能給你把所有屬性都緩存到這個(gè)對(duì)象中呢?
我們?cè)谥v之前的例子中,提到我們關(guān)閉session再打開(kāi),這個(gè)時(shí)候一級(jí)緩存就不存在了,所以我們?cè)俅尾樵兊臅r(shí)候,會(huì)再次發(fā)送SQL語(yǔ)句。那么如果要解決這個(gè)問(wèn)題,我們?cè)撛趺醋觯慷?jí)緩存可以幫我們解決這個(gè)問(wèn)題。
Hibernate中沒(méi)有自己去實(shí)現(xiàn)二級(jí)緩存,而是利用第三方的。簡(jiǎn)單敘述一下配置過(guò)程,也作為自己以后用到的時(shí)候配置的一個(gè)參考。
1、我們需要加入額外的二級(jí)緩存包,例如EHcache,將其包導(dǎo)入。需要:ehcache-core-2.4.3.jar , hibernate-ehcache-4.2.4.Final.jar ,slf4j-api-1.6.1.jar 2、在hibernate.cfg.xml配置文件中配置我們二級(jí)緩存的一些屬性(此處針對(duì)的是Hibernate4):
<!-- 啟用二級(jí)緩存 -->
<property name="cache.use_second_level_cache">true</property>
<!-- 配置使用的二級(jí)緩存的產(chǎn)品 -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
3、我們使用的是EHcache,所以我們需要?jiǎng)?chuàng)建一個(gè)ehcache.xml的配置文件,來(lái)配置我們的緩存信息,這個(gè)是EHcache要求的。該文件放到根目錄下。
<ehcache>
<!--
指定一個(gè)目錄:當(dāng) EHCache 把數(shù)據(jù)寫(xiě)到硬盤(pán)上時(shí), 將把數(shù)據(jù)寫(xiě)到這個(gè)目錄下.
-->
<diskStore path="d:\\tempDirectory"/>
<!--Default Cache configuration. These will applied to caches programmatically created through
the CacheManager.
The following attributes are required for defaultCache:
maxInMemory - Sets the maximum number of objects that will be created in memory
eternal - Sets whether elements are eternal. If eternal, timeouts are ignored and the element
is never expired.
timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only used
if the element is not eternal. Idle time is now - last accessed time
timeToLiveSeconds - Sets the time to live for an element before it expires. Is only used
if the element is not eternal. TTL is now - creation time
overflowToDisk - Sets whether elements can overflow to disk when the in-memory cache
has reached the maxInMemory limit.
-->
<!--
設(shè)置緩存的默認(rèn)數(shù)據(jù)過(guò)期策略
-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
<!--
設(shè)定具體的命名緩存的數(shù)據(jù)過(guò)期策略。每個(gè)命名緩存代表一個(gè)緩存區(qū)域
緩存區(qū)域(region):一個(gè)具有名稱的緩存塊,可以給每一個(gè)緩存塊設(shè)置不同的緩存策略。
如果沒(méi)有設(shè)置任何的緩存區(qū)域,則所有被緩存的對(duì)象,都將使用默認(rèn)的緩存策略。即:<defaultCache.../>
Hibernate 在不同的緩存區(qū)域保存不同的類/集合。
對(duì)于類而言,區(qū)域的名稱是類名。如:com.atguigu.domain.Customer
對(duì)于集合而言,區(qū)域的名稱是類名加屬性名。如com.atguigu.domain.Customer.orders
-->
<!--
name: 設(shè)置緩存的名字,它的取值為類的全限定名或類的集合的名字
maxElementsInMemory: 設(shè)置基于內(nèi)存的緩存中可存放的對(duì)象最大數(shù)目
eternal: 設(shè)置對(duì)象是否為永久的, true表示永不過(guò)期,
此時(shí)將忽略timeToIdleSeconds 和 timeToLiveSeconds屬性; 默認(rèn)值是false
timeToIdleSeconds:設(shè)置對(duì)象空閑最長(zhǎng)時(shí)間,以秒為單位, 超過(guò)這個(gè)時(shí)間,對(duì)象過(guò)期。
當(dāng)對(duì)象過(guò)期時(shí),EHCache會(huì)把它從緩存中清除。如果此值為0,表示對(duì)象可以無(wú)限期地處于空閑狀態(tài)。
timeToLiveSeconds:設(shè)置對(duì)象生存最長(zhǎng)時(shí)間,超過(guò)這個(gè)時(shí)間,對(duì)象過(guò)期。
如果此值為0,表示對(duì)象可以無(wú)限期地存在于緩存中. 該屬性值必須大于或等于 timeToIdleSeconds 屬性值
overflowToDisk:設(shè)置基于內(nèi)存的緩存中的對(duì)象數(shù)目達(dá)到上限后,是否把溢出的對(duì)象寫(xiě)到基于硬盤(pán)的緩存中
-->
<cache name="com.atguigu.hibernate.entities.Employee"
maxElementsInMemory="1"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
<cache name="com.atguigu.hibernate.entities.Department.emps"
maxElementsInMemory="1000"
eternal="true"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
/>
</ehcache>
在注釋中,有一些對(duì)變量的解釋。
4、開(kāi)啟二級(jí)緩存。我們?cè)谶@里使用的xml的配置方式,所以要在Customer.hbm.xml文件加一點(diǎn)配置信息:
<cache usage="read-only"/>
注意是在標(biāo)簽內(nèi)。
如果是使用注解的方法,在要在Customer這個(gè)類中,加入@Cache(usage=CacheConcurrencyStrategy.READ_ONLY)
這個(gè)注解。
5、下面我們?cè)龠M(jìn)行一下測(cè)試。還是上面的代碼:
@Test
public void test() {
Customer customer1 = (Customer) session.load(Customer.class, 1);
System.out.println(customer1.getCustomerName());
transaction.commit();
session.close();
session = sessionFactory.openSession();
transaction = session.beginTransaction();
Customer customer2 = (Customer) session.load(Customer.class, 1);
System.out.println(customer2.getCustomerName());
}
我們可以發(fā)現(xiàn)控制臺(tái)只發(fā)出了一條SQL語(yǔ)句。這是我們二級(jí)緩存的一個(gè)小Demo。
我們的二級(jí)緩存是sessionFactory級(jí)別的,所以當(dāng)我們session關(guān)閉再打開(kāi)之后,我們?cè)偃ゲ樵儗?duì)象的時(shí)候,此時(shí)Hibernate會(huì)先去二級(jí)緩存中查詢是否有該對(duì)象。
同樣,二級(jí)緩存緩存的是對(duì)象,如果我們查詢的是對(duì)象的一些屬性,則不會(huì)加入到緩存中。
我們通過(guò)二級(jí)緩存是可以解決之前提到的N+1問(wèn)題。
已經(jīng)寫(xiě)了這么多了,但好像我們關(guān)于緩存的內(nèi)容還沒(méi)有講完。不要著急,再堅(jiān)持一下,我們的內(nèi)容不多了。我們還是通過(guò)一個(gè)例子來(lái)引出下一個(gè)話題。 我們說(shuō)通過(guò)二級(jí)緩存可以緩存對(duì)象,那么我們看一下下面的代碼以及輸出結(jié)果:
@Test
public void test() {
List<Customer> customers1 = session.createQuery("from Customer").list();
System.out.println(customers1.size());
tansaction.commit();
session.close();
session = sessionFactory.openSession();
transaction = session.beginTransaction();
List<Customer> customers2 = session.createQuery("from Customer").list();
System.out.println(customers2.size());
}
控制臺(tái)的結(jié)果:
Hibernate:
select
customer0_.CUSTOMER_ID as CUSTOMER1_0_,
customer0_.CUSTOMER_NAME as CUSTOMER2_0_
from
CUSTOMERS customer0_
3
Hibernate:
select
customer0_.CUSTOMER_ID as CUSTOMER1_0_,
customer0_.CUSTOMER_NAME as CUSTOMER2_0_
from
CUSTOMERS customer0_
3
我們的緩存好像沒(méi)有起作用哎?這是為啥?當(dāng)我們通過(guò)list()去查詢兩次對(duì)象的時(shí)候,二級(jí)緩存雖然會(huì)緩存插敘出來(lái)的對(duì)象,但不會(huì)緩存我們的hql查詢語(yǔ)句,要想解決這個(gè)問(wèn)題,我們需要用到查詢緩存。
在前文中也提到了,我們的一級(jí)二級(jí)緩存都是對(duì)整個(gè)實(shí)體進(jìn)行緩存,它不會(huì)緩存普通屬性,如果想對(duì)普通屬性進(jìn)行緩存,則可以考慮使用查詢緩存。
但需要注意的是,大部分情況下,查詢緩存并不能提高應(yīng)用程序的性能,甚至反而會(huì)降低應(yīng)用性能,因此實(shí)際項(xiàng)目中要謹(jǐn)慎的使用查詢緩存。
對(duì)于查詢緩存來(lái)說(shuō),它緩存的key就是查詢所用的HQL或者SQL語(yǔ)句,需要指出的是:查詢緩存不僅要求所使用的HQL、SQL語(yǔ)句相同,甚至要求所傳入的參數(shù)也相同,Hibernate才能直接從緩存中取得數(shù)據(jù)。只有經(jīng)常使用相同的查詢語(yǔ)句、并且使用相同查詢參數(shù)才能通過(guò)查詢緩存獲得好處,查詢緩存的生命周期直到屬性被修改了為止。
查詢緩存默認(rèn)是關(guān)閉。要想使用查詢緩存,只需要在hibernate.cfg.xml中加入一條配置即可:
<property name="hibernate.cache.use_query_cache">true</property>
而且,我們?cè)诓樵僪ql語(yǔ)句時(shí),要想使用查詢緩存,就需要在語(yǔ)句中設(shè)置這樣一個(gè)方法:setCacheable(true)
。關(guān)于這個(gè)的demo我就不進(jìn)行演示了,大家可以自己慢慢試著玩一下。
但需要注意的是,我們?cè)陂_(kāi)啟查詢緩存的時(shí)候,也應(yīng)該開(kāi)啟二級(jí)緩存。因?yàn)槿绻皇褂枚?jí)緩存,也有可能出現(xiàn)N+1的問(wèn)題。
這是因?yàn)椴樵兙彺婢彺娴膬H僅是對(duì)象的ID,所以首先會(huì)通過(guò)一條SQL將對(duì)象的ID都查詢出來(lái),但是當(dāng)我們后面要得到每個(gè)對(duì)象的信息的時(shí)候,此時(shí)又會(huì)發(fā)送SQL語(yǔ)句,所以如果我們使用查詢緩存,一定也要開(kāi)啟二級(jí)緩存。
這些就是自己今晚上研究的關(guān)于Hibernate緩存的一些問(wèn)題,其出發(fā)點(diǎn)也是為了自己能夠?qū)ibernate緩存的知識(shí)有一定的總結(jié)。當(dāng)然了,下一步還需要深入到緩存是如何實(shí)現(xiàn)的這個(gè)深度中。
另外PS一句,最近打球打的很累,都感覺(jué)自己打的有點(diǎn)乏力了。休息幾天再去玩。
更多建議: