Hibernate中的 3-2-1

one-to-one有三種方法來做

one-to-many有兩種方法

many-to-many有一種方法。

單向的many-to-one 與單向的one-to-one的寫法是一樣的。因爲單向的one-to-one是在一個表裏設置了外健。有了外健所以one-to-one就要改成many-to-one不過,加了一個屬性。Unique=”true這是第一種特殊的情況。
one-to-many
說一下單向的one-to-many,單向也就是隻在一邊設置關係,另一邊不知道。雙向就是兩個實體類都有另一表的屬性。
比如一個人有多個地址。單向的one-to-many就是隻在one這個進行設置。

<set >

<key column="personId" />

<one-to-many />

</set> 單向的one-to-many設置方法
這個addresses是在person類裏面加的一個set屬性的名子。這個集合其實在person表裏根本就沒有的。只是爲了做關係。Key就是指明這個集合的外健。意思好像是本類在用key找另一個表的外健。那個外健也就是本表的主健。下面指明是one-to-many的關係。指明對應的類。因爲這個程序就是加載多個Address類來實現one-to-many
首先在person裏面要多加一個屬性就是一個集合,然後對這個集合進行配製。
昨天寫了在hibernate中的值映射。用樓與單元做的例子。一個樓有多個單元,所以就把這個單元做到另一張表中。在hibernate中映射時,就只建一個樓的實體bean,裏面有一個屬性是list或set的屬性單元。然後在映射關係中把樓加上就做成了值的映射。現在來寫一個關係的映射。
Many-to-one
這個關係中最簡單的一種:
有兩張表。students與teachers,students爲many.所以外健在students中。
建兩個實體,一個student實體,因爲在student裏面有外健,但是不是在實體裏面直接加一個外健id就可以的。要變成在Student這個實體中加入一個Teacher對象作爲屬性。配製文件是這樣寫的

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" >

<hibernate-mapping >

<class name="vo.Student" table="students">

<id column="id">

<generator />

</id>

<property type="string" />

<property type="int" />

用這一個代表關係,意思是外健在這個表中,對應在表中的字段是tea_id這個很簡單

<many-to-one column="tea_id" />


</class>

<class table="teachers">

<id >

<generator />

</id>

<property />

<property />

</class>

</hibernate-mapping>
One-to-one
l 一對一關係有三種實現方式:
l 一是通過外鍵方式實現,即當前對象持有關聯對象的外鍵;
l 二是通過主鍵方式實現,即當前對象的主鍵即是主鍵也是外鍵,也就是說當前對象的主鍵和與之相關聯對象的主鍵是相同的。
l 三是通過關係表實現,即關聯雙方都以外鍵的形式存儲在關係表中,通過關係表反映一對一的關係,這種方式很少使用

這是關係表中最爲複雜的一個關係
這個是雙向的外健方式實現的,
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="vo">

<class table="students">
<id column="id">
<generator />
</id>
<property />
<property />
<many-to-one column="tea_id" lazy="no-proxy" unique="true"/>
</class>
<class table="teachers">
<id >
<generator />
</id>
<property />
<property />
在沒有處健的表中也要定義Student student這個屬性。來產生一對一的關係。
<one-to-one property-ref="teacher" />

</class>
</hibernate-mapping>
這是一個單外主健方式來實現的。因爲我們要先存teacher.所有在teacher裏面設置一個屬性student,在存teacher的時候同時也給student的id進行附值。

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" >

<hibernate-mapping >

<class name="vo.Student" table="students">

<id column="id">

<generator />

</id>

<property type="string" />

<property type="int" />

</class>

<class table="teachers">

<id >

這個主健同時也做外健。

<generator >

<param >student</param>

</generator>

</id>

<property />

<property />

關係爲一對一,它會影響student.所對應的表。

<one-to-one c/>

</class>

</hibernate-mapping>


下面這個配製是一對一用關係表的方式實現。可以看來。如果關係真的是一對一的話用上面的主健是最正常的,可是用多對一是最簡單的。下面這一些是最麻煩的。

首先是建一個關係表。關係表裏面沒有主健,放的是兩個表的主健。

然後在配製文件中在兩個表的配製中同時映射到關係表上,

<join table=”關係表 optional=”true”>這個optional的理解爲update就是如果不加這個的話會執行插入操作完了以後在執行update,反正這東西我理解的也不清楚。還有一個屬性是inverse這個如果設爲true的話就是

兩個特例,one-to-one用關係表來的做的。持有外健的一方要變爲many-to-one.

第二個是one-to-many的時候。要改成many-to-many

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping PUBLIC

"-//Hibernate/Hibernate Mapping DTD 3.0//EN"

"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="vo">


<class table="students">

<id column="id">

<generator />

</id>

<property />

<property />

<join table="stu_tea" optional="true">

<key column="stu_id" />

<many-to-one column="tea_id" />

</join>

</class>

<class table="teachers">

<id >

<generator />

</id>

<property />

<property />

<join table="stu_tea" optional="true" inverse="true">

<key column="tea_id" />

<many-to-one column="stu_id" />

</join>

</class>

</hibernate-mapping>

補上
hibernate作爲目前傑出的O-R mapping 工具,對對象間關係的良好映射支持也是其一大核心。
對象間關係:在面向對象設計和實體模型關係中,一般有四種關係:一對一(one-to-one)、一對多(one-to-many)、多對一(many-to-one)、多對多(many-to-many)。
Hibernate的對象映射中,對這四種關係有着比較全面的支持。
在描述時,我們假定有兩個對象:A和B;所指的hibernate中的映射一般是指 hibernate的.hbm.xml映射文件中的映射描述;對象模型只是用僞語言進行描述。Collection 類型可以是任何類型,除非針對具體的語言進行解釋;關係分爲單向和雙向。以下四種關係都可以作爲單向和雙向兩種映射配置。
考慮問題時,請在腦海中想象:A作爲關聯的左端,B作爲關聯的右端,這樣使得關聯關係有立體感。

1、一對一(one-to-one)
關係舉例:即A->B之間是一一對應的關係。比如一個國家有一個總統,一個總統只能是一個國家的總統。
class A{B b;}
class B{A a;}
A和B的關係是一一對應的關係,但是A不需要
Hibernate中映射:
A的映射:
<one-to-one />
B的映射:
<many-to-one column="a_ID" />
實現說明:
兩個表:數據庫表兩個,A對應的表和B對應的表;a表中不包含 B的任何信息,但是B中要包含A的id,即a_id。
如果實現時,只在A中做了<one-to-one /> 映射描述,而B中沒有做映射描述,我們稱之爲單向關係。
Person person = new Person();
person.setName("newps");
Account account = new Account();
//other goes here......
person.setAccount(account);

//session.save(account);
session.save(person);
但是如果只在任意一個映射中做了 <many-to-one /> 的映射,而沒有做其他的映射,並且 另外一個對象沒有序列華時,hibernate則會給出異常信息。

2、一對多(one-to-many)
關係舉例:A->之間是一對多的關係,比如一個老師可以教授多名學生。
對象模型:
class A{ Collection bs;}
class B{A a;}
Hibernate中映射:
A的映射:
<bag cascade="all">
<key column="a_id"/> <!-- 指 b對應的表中存放a的id的字段,關係的左端-->
<one-to-many ></one-to-many><關係的右端>
</bag>
B的映射:
<many-to-one column="a_id"/> <!-- 指 b對應的表中存放a的id的字段-->

實現說明:
數據庫表兩個,A對應的表和B對應的表;
如果 Collection 是List 這裏用bag,如果是Set 則用set。兩者的區別主要是 set是無序的。
而且在用in 等查詢的時候,只能用於list。

3、多對一(many-to-one)
關係舉例:比如多個學生可以由一個老師教授。
多對一和一對多 是互逆關係。對於2 中的B來說,B和A之間的關係就是多對一。

4、多對多(many-to-many)
關係舉例:比如一所大學可以有多名教授,而教授也可以在多所大學任教。
對象模型:
class A{ List bs;}
class B{ List as;}
Hibernate中的映射:
<bag table="a" cascade="save-update" inverse="true">
<key column="a_id"/>
<many-to-many column="b_id"/>
</bag>
<bag table="a" inverse="false" >
<key column="b_id"/>
<many-to-many column="a_id" />
</bag>
實現說明:
數據庫表三個,A對應的表和B對應的表;以及A和B的關聯表ab,ab中的字段是(a_id,b_id);
需要注意的是inverse的值必須是true和false兩個能形成直線,也就是說,a中是true,b只能是false。
在關係的映射中,可以加入cascade 的屬性決定是否級聯操作。

其實 Hibernate對關係的映射支持非常靈活。同時,在面向對象的建模中,由於這些映射關係的支持,使得我們可以大膽的建立對象間的映射,而不用怎麼擔心對象的存取。真正取體會面向對象設計的方便和樂趣。
cascade
一對多關聯關係的使用
一對多關係很覺,例如班級與學生的關係就是典型的一對多的關係。在實際編寫程序時,一對多關係有兩種實現方式:單向關聯和雙向關聯。單向的一對多關係只需要在一方進行映射配置,而雙向的一對多需要在關聯的雙方進行映射配置。下面以Group(班級)和Student(學生)爲例講解如何配置一對多的關係。
單向關聯:
單向的一對多關係只需要在一方進行映射配置,所以我們只配置Group的映射文件: <hibernate-mapping>
<class table="t_group" lazy="true">
<id type="java.lang.Integer">
<column />
<generator />
</id>
<!-- insert屬性表示被映射的字段是否出現在SQL的INSERT語句中 -->
<property type="java.lang.String" update="true" insert="true">
<column length="20" />
</property>

<!-- set元素描述的字段對應的類型爲java.util.Set類型。
inverse用於表示雙向關聯中的被動一端。inverse的值
爲false的一方負責維護關聯關係。
sort排序關係,其可選值爲:unsorted(不排序)。
natural(自然排序)。
comparatorClass(由某個實現了java.util.comparator接口的類型指定排序算法。)
<key>子元素的column屬性指定關聯表(t_student表)的外鍵。
-->
<set
table="t_student"
lazy="true"
inverse="false"
cascade="all"
sort="unsorted">
<key column="ID"/>
<one-to-many />
</set>
</class>
</hibernate-mapping>雙向關聯:
如果要設置一對多雙向關聯關係,那麼還需要在“多”方的映射文件中使用<many-to-one>標記。例如,在Group與Student一對多的雙向關聯中,除了Group的映射文件外還需要在Student的映射文件中加入如下代碼: <many-to-one

cascade="none"
outer-join="auto"
update="true"
insert="true"
column="ID" />
inert和update設定是否對column屬性指定的關聯字段進行insert和update操作。
此外將Group.hbm.xml中<set>元素的inverse設置爲true.

多對多關聯關係的使用
Student(學生)和Course(課程)的關係就是多對多關係。在映射多對多關係時需要另外使用一個連接表(如Student_Course)。Student_Course表包含二個字段:courseID和studentID。此處它們的映射文件中使用<many-to-many>標記,在Student的映射文件中加入以下描述信息:

<set
table="student_course"
lazy="false"
inverse="false"
cascade="save-update">
<key column="studentID" />
<many-to-many column="CourseID"/>
</set>相應的Course的映射文件中加入以下: <set
table="student_course"
lazy="false"
inverse="true"
cascade="save-update">
<key column="CourseID" />
<many-to-many column="StudentID"/>
</set>添加關聯關係:
首先編寫一個程序來看看一個名爲Bill的學生選擇了什麼課程: //獲取代表Bill的Student對象
Student stu = (Student)session.createQuery("from Student s where s.name='Bill'").uniqueResult();
List list = new ArrayList(stu.getCourses());
for(int i = 0 ; i < list.size(); i++)
{
Course course = (Course)list.get(i);//取得Course對象
System.out.println(course.getName());//打印出Bill所選課程的清單
}現在Bill還想chemistry課程,這對於程序員來說只是爲Bill添加一個到chemistry的關聯,也就是說在student_course表中新增加一條記錄,而T_student和T_Course表都不用變更。 //獲取代表Bill的Student對象
Student stu = (Student)session.createQuery("from Student s where s.name='Bill'").uniqueResult();
Course course = (Course)session.createQuery("from Course c where c.name='chemistry'").uniqueResult();
//設置stu與course的關聯關係
stu.getCourses().add(course);
course.getStudents().add(stu);刪除關聯關係:
刪除關聯關係比較簡單,直接調用對象集合的remove()方法刪除不要的對象就可。例如:要從學生Bill的選課清單中刪除politics和chemistry兩門課,程序代碼如下: //獲取代表Bill的Student對象
Student stu = (Student)session.createQuery("from Student s where s.name='Bill'").uniqueResult();
Course course1 = (Course)session.createQuery("from Course c where c.name='politics'").uniqueResult();
Course course2 = (Course)session.createQuery("from Course c where c.name='chemistry'").uniqueResult();
stu.getCourse().remove(course1);//刪除politics課程
stu.getCourse().remove(course2);//刪除chemistry課程運行以上程序將從student_course表中刪除這兩條記錄,但T_student和T_course表沒有任何變化
Hibernate關係映射的說明 老外精彩的解釋
Hibernate關係映射的說明
開源框架對數據層的映射包括實體的映射和關係的映射,其中關係的映射是最複雜的。如果你掌握不好關係映射,你乾脆就不要用,否則會嚴重地影響性能。   

Hibernate中的實體關係分爲四種,基本上與我們在數據庫建模中瞭解到的實體關係是一樣的,即一對一關係、一對多關係、多對一關係、多對多關係。我們下面分別給出說明:   

一、一對一關係、多對一關係   

一對一、多對一關係在代碼上的體現是在JavaBean中包含一個實體類屬性。比如,夫妻關係是一對一的關係,那麼丈夫的屬性中就應該有一個屬性是妻子,妻子的屬性中也應該有一個屬性是丈夫。同樣,多對一的關係中,在代碼上的體現也是在Java中包含一個實體類屬性。比如,孩子與媽媽的關係就是多對一的關係,每一個孩子都應該有一個屬性是媽媽。我們發現,無論是一對一,還是多對一,它們在代碼是都是一樣的,就是屬性中包含一個實體類屬性。而事實上,一對一關係是多對一關係的一種特例而已。所以,在映射時,由外鍵實現的一對一關係或多對一關係時,無論是哪一種,外鍵所在一方關係屬性都是通過many-to-one映射的,而不是用one-to-one。   

二、一對多關係、多對多關係

這兩種關係也有些共性,就是它們在代碼上的體現都是在JavaBean中包含一個集合屬性。比如在上面說的媽媽與孩子的關係,媽媽應該包含一個集體屬性,在這個集合中包含了她所有的小孩。這種一對多關係使用one-to-many來映射,在數據庫中的體現是在一的一方是不含有外鍵的。而多對多的關係雖然也是在屬性中包含一個集合屬性,但在映射時使用的卻是many-to-many。這主要是因爲多對多關係需要一個關係表,必須告訴Hibernate這個關係表是誰。   

以上只是簡單的概括了一下Hibernate中一些關係映射的特點,下面來說說Hibernate關係映射中的難點問題。   

如果不是真正使用Hibernate開發過項目,恐怕很難理解爲什麼說如果掌握不好關係最好不要使用關係。這裏面有這樣幾個問題:

1、關係的級聯操作問題。  

我們知道,在數據庫中,建立外鍵的同時還要建立約束關係。這就存在一個問題,即當外鍵所指向表的行被刪除時,當前行應該如何操作。比如,丈夫表含有一個外鍵指向妻子,如果妻子被刪除了,那麼丈夫所在行的外鍵的值應該如何操作?如果保持原有值不變,但他所指向的妻子卻已經不在了,這樣的記錄就沒有意義了,所以必須要採取一些行爲。在數據庫中一般會根據用戶的設置採取三種行爲,一是給出提示,告訴你因爲有外鍵指向這個行,所以不能刪,如果非要刪除,必須先將指向它的外鍵卻除;二是在刪除當前行後,自動將指向它的外鍵置爲空;三是刪除當前行後,將所有指向它的行也同時刪除。  

一般數據庫默認情況下都採取第一種行爲,這樣如果不做任何設置,當我們對一個實體進行刪除時,就會報錯,告訴你有外鍵指向不能刪除。  

這種問題如何解決呢?你可以事先做好數據庫設計,讓數據庫幫你採取一種更合適的行爲。兩種可選的確定行爲,即置空或級聯刪除。究竟採用哪一種更合適,要視你的應用而定,限於篇幅,我這裏就不做過多的講解了。再者,可以使用Hibernate的cascade屬性,它的delete代表刪除時置空外鍵,而delete-orphan則代表刪除時同時刪除指向它的行。

2、關係的方向性問題 

“一個巴掌拍不響”,一個關係也一定有兩個實體。這就存在了另外一個問題,當一個實體發生變化時,關係是不是也一定要跟着變化?這種問題很難說清,因爲它跟具體的應用關聯。有時,我們只要更新實體,而不想更新關係;而有時我們又想更新關係而不更新實體;還有些情況下,我們是實體和關係同時都要更新。在默認情況下,Hibernate對於實體和關係是同時更新的,即使你根本沒有更改過關係。這對於性能的影響比較大。我們可以給關係設置一個inverse屬性,告訴它在任何變化下,都不要更新關係。當然還有其它的辦法,讀者可以參考其文檔。總之,這是一個非常複雜的問題,要視你的應用而定。

3、N+1查詢問題  

關於什麼是N+1查詢,我不想解釋,讀者可以看一下我前面的文章或者到網上去查詢。總的來說N+1查詢的問題就是性能太低,在有些情況下甚至會導致系統崩潰。但有些時候它又是有益的。因爲N+1查詢實際上是延遲加載了,它節省了空間。Hibernate有一個fetch屬性,用於說明抓取數據的策略,如果選擇了join則不會使用N+1查詢,但加載上來了所有的數據並不一定都是你想要的,也可能會浪費存儲空間。

4、延遲加載  

延遲加載就是並不是在讀取的時候就把數據加載進來,而是等到使用時再加載。那麼Hibernate是怎麼知識用戶在什麼時候使用數據了呢?又是如何加載數據呢?其實很簡單,它使用了代理機制。返回給用戶的並不是實體本身,而是實體對象的代理。代理對象在用戶調用getter方法時就會去數據庫加載數據。但加載數據就需要數據庫連接。而當我們把會話關閉時,數據庫連接就同時關閉了。這種情況就叫做未初始化的關係。 

延遲加載的好處就是節省了存儲空間。因爲我們並不是在所有情況下都需要關係數據。比如,媽媽和孩子。如果你只想修改媽媽的數據,而Hibernate將她10幾個孩子也同時給你加載進來了,這顯然是無意義的。所以你可以使用Hibernate.initialize()方法主動地去決定是否初始化關係。當然也可以在配置文件中通過lazy屬性,但這樣一來就固定了,要麼延遲,要麼不延遲。   

Hibernate的關係還有很多問題,這裏限於篇幅先講這麼多。還是開頭的那句話,如果你沒有掌握好Hibernate中關係的映射,那你乾脆就不要用了,否則嚴重地影響性能。
如何學習Hibernate
Hibernate入門容易,掌握精通我也不敢自誇。我第一遍看Hibernate文檔的時候也覺得很吃力,但不是因爲Hibernate難掌握而感到吃力,是因爲Hibernate文檔處處都是持久層設計的經驗和最佳實踐。Hibernate文檔準確的來說,絕大部分內容都在講對象的持久層設計,而不是簡單的Hibernate使用,使用問題查Java doc就夠了。所以學習Hibernate,主要是在學習持久層的設計模式,如果你把Hibernate文檔都看完了,還整天只會提那些 Hibernate的配置問題,Hibernate的類調用問題,我覺得這樣的人還沒有真正的入門,算是白學了。
我對Hibernate 的那些配置也不是特別純熟,每次寫hbm,都要對照文檔一點點的檢查;類調用參數也不太記得,寫代碼也要Java doc隨時備查。但是我在學習Hibernate的時候即集中所有精力來理解Hibernate的運行原理,集中精力來掌握持久層設計應該把握的原則和技巧,這些纔對我是最重用的東西。毫不誇張的說,學習完Hibernate,我對JDBC的編程也提高了一大截,更不要說對於J2EE架構的持久層的框架設計,基本上是瞭然於胸了,即使將來換了API,不用Hibernate的,改用JDO,Castor什麼的,這些經驗一樣照用。
學習Hibernate主要不是在學習Hibernat怎麼配置,用工具怎麼生成hbm文件,如果你把重點放在這裏,基本上等於白學了Hibernate。Hibernate的精華在於無與倫比的靈巧的對象持久層設計,這些持久層設計經驗不會因爲你不用Hibernate而喪失掉,我自己學習Hibernate,已經明顯感覺到對持久層設計能力已經長了很多經驗值了,這些經驗甚至不光可以用在Java上,用在.net上也是一樣。所以Hibernate配置的學習,我只是簡單看看,用的時候知道到那裏去查就行了,一堆複雜的生成工具我根本就看都不去看,這樣算下來,掌握Hibernate的配置,可以用Hibernate來替代JDBC寫程序,不過花上3天時間就足夠了。我想3天時間對你來說不算很奢侈的學習代價吧。
爲什麼我這麼強調學習Hibernate的對象持久層設計理念呢?那就看你的理想是想一輩子做一個程序員呢?還是想向更高的方向發展呢?從純做技術的角度來說,職業發展的最高點是“系統架構師”,Bill Gates不是還叫做微軟的首席系統架構師嗎?System Architect職位需要的是你的學習和領悟能力,如果你不能把學習Hibernate得到的設計經驗運用到其它地方,那麼你是失敗的,也沒有資格做 System Architect。
不管JDO也好,Hibernate也好,TopLink也好,CocoBase也好,還是 Castor,還是什麼Torque,OJB,軟件的使用和配置方法可以各異,但本質上都是ORM,都是對JDBC的對象持久層封裝,所以萬變不離其宗,如果你完整的學習和掌握Hibernate花了1個月的時間,那麼你再學習OJB的時間不應該超過1個星期,因爲你已經把對象持久層設計都瞭然於胸了,你需要的只是熟悉一下OJB的API和配置罷了,至於怎麼運用OJB進行持久層的開發你早就已經熟悉了。
所以當你掌握了兩種以上的ORM,你應該能夠不拘於使用的ORM軟件的限制,設計出適合於你的項目的持久層來,這纔是System Architect的水準。用金庸小說來打個比方來說吧,張無忌學太極劍,只記劍意,不記劍招,這纔是真正的高手,而低手就只會去學習劍招,而不去領會劍招背後蘊含的劍意,所以一輩子都是低手,永遠不能真正學會太極劍。所以周顛看到張三丰第二次演示太極劍,招式完全不同就以爲是另一套東西,其實本質上都一樣。學習Hibernate也不要捨本逐末的去學各種五花八門的工具,重點掌握它的對象持久層設計理念。
Hibernate查詢方法與緩存的關係
在開發中,通常是通過兩種方式來執行對數據庫的查詢操作的。一種方式是通過ID來獲得單獨的Java對象,另一種方式是通過HQL語句來執行對數據庫的查詢操作。下面就分別結合這兩種查詢方式來說明一下緩存的作用。
通過ID來獲得Java對象可以直接使用Session對象的load()或者get()方法,這兩種方式的區別就在於對緩存的使用上。
● load()方法
在使用了二級緩存的情況下,使用load()方法會在二級緩存中查找指定的對象是否存在。
在執行load()方法時,Hibernate首先從當前Session的一級緩存中獲取ID對應的值,在獲取不到的情況下,將根據該對象是否配置了二級緩存來做相應的處理。
如配置了二級緩存,則從二級緩存中獲取ID對應的值,如仍然獲取不到則還需要根據是否配置了延遲加載來決定如何執行,如未配置延遲加載則從數據庫中直接獲 取。在從數據庫獲取到數據的情況下,Hibernate會相應地填充一級緩存和二級緩存,如配置了延遲加載則直接返回一個代理類,只有在觸發代理類的調用 時才進行數據庫的查詢操作。
在Session一直打開的情況下,並在該對象具有單向關聯維護的時候,需要使用類似Session.clear(),Session.evict()的方法來強制刷新一級緩存。
● get()方法
get()方法與load()方法的區別就在於不會查找二級緩存。在當前Session的一級緩存中獲取不到指定的對象時,會直接執行查詢語句從數據庫中獲得所需要的數據。
在Hibernate中,可以通過HQL來執行對數據庫的查詢操作。具體的查詢是由Query對象的list()和iterator()方法來執行的。這兩個方法在執行查詢時的處理方法存在着一定的差別,在開發中應該依據具體的情況來選擇合適的方法。
● list()方法
在執行Query的list()方法時,Hibernate的做法是首先檢查是否配置了查詢緩存,如配置了則從查詢緩存中尋找是否已經對該查詢進行了緩 存,如獲取不到則從數據庫中進行獲取。從數據庫中獲取到後,Hibernate將會相應地填充一級、二級和查詢緩存。如獲取到的爲直接的結果集,則直接返 回,如獲取到的爲一些ID的值,則再根據ID獲取相應的值(Session.load()),最後形成結果集返回。可以看到,在這樣的情況下,list ()方法也是有可能造成N次查詢的。
查詢緩存在數據發生任何變化的情況下都會被自動清空。
● iterator()方法
Query的iterator()方法處理查詢的方式與list()方法是不同的,它首先會使用查詢語句得到ID值的列表,然後再使用Session的load()方法得到所需要的對象的值。
在獲取數據的時候,應該依據這4種獲取數據方式的特點來選擇合適的方法。在開發中可以通過設置show_sql選項來輸出Hibernate所執行的SQL語句,以此來了解Hibernate是如何操作數據庫的。
Hibernate緩存
1. 關於hibernate緩存的問題:
1.1.1. 基本的緩存原理
Hibernate緩存分爲二級,
第一級存放於session中稱爲一級緩存,默認帶有且不能卸載。
第二級是由sessionFactory控制的進程級緩存。是全局共享的緩存,凡是會調用二級緩存的查詢方法 都會從中受益。只有經正確的配置後二級緩存纔會發揮作用。同時在進行條件查詢時必須使用相應的方法才能從緩存中獲取數據。比如Query.iterate()方法、load、get方法等。必須注意的是session.find方法永遠是從數據庫中獲取數據,不會從二級緩存中獲取數據,即便其中有其所需要的數據也是如此。
查詢時使用緩存的實現過程爲:首先查詢一級緩存中是否具有需要的數據,如果沒有,查詢二級緩存,如果二級緩存中也沒有,此時再執行查詢數據庫的工作。要注意的是:此3種方式的查詢速度是依次降低的。
1.2. 存在的問題
1.2.1. 一級緩存的問題以及使用二級緩存的原因
因爲Session的生命期往往很短,存在於Session內部的第一級最快緩存的生命期當然也很短,所以第一級緩存的命中率是很低的。其對系統性能的改善也是很有限的。當然,這個Session內部緩存的主要作用是保持Session內部數據狀態同步。並非是hibernate爲了大幅提高系統性能所提供的。
爲了提高使用hibernate的性能,除了常規的一些需要注意的方法比如:
使用延遲加載、迫切外連接、查詢過濾等以外,還需要配置hibernate的二級緩存。其對系統整體性能的改善往往具有立竿見影的效果!
(經過自己以前作項目的經驗,一般會有3~4倍的性能提高)
1.2.2. N+1次查詢的問題
執行條件查詢時,iterate()方法具有著名的 “n+1”次查詢的問題,也就是說在第一次查詢時iterate方法會執行滿足條件的查詢結果數再加一次(n+1)的查詢。但是此問題只存在於第一次查詢時,在後面執行相同查詢時性能會得到極大的改善。此方法適合於查詢數據量較大的業務數據。
但是注意:當數據量特別大時(比如流水線數據等)需要針對此持久化對象配置其具體的緩存策略,比如設置其存在於緩存中的最大記錄數、緩存存在的時間等參數,以避免系統將大量的數據同時裝載入內存中引起內存資源的迅速耗盡,反而降低系統的性能!!!
1.3. 使用hibernate二級緩存的其他注意事項:
1.3.1. 關於數據的有效性
另外,hibernate會自行維護二級緩存中的數據,以保證緩存中的數據和數據庫中的真實數據的一致性!無論何時,當你調用save()、update()或 saveOrUpdate()方法傳遞一個對象時,或使用load()、 get()、list()、iterate() 或scroll()方法獲得一個對象時, 該對象都將被加入到Session的內部緩存中。 當隨後flush()方法被調用時,對象的狀態會和數據庫取得同步。
也就是說刪除、更新、增加數據的時候,同時更新緩存。當然這也包括二級緩存!
只要是調用hibernate API執行數據庫相關的工作。hibernate都會爲你自動保證 緩存數據的有效性!!
但是,如果你使用了JDBC繞過hibernate直接執行對數據庫的操作。此時,Hibernate不會/也不可能自行感知到數據庫被進行的變化改動,也就不能再保證緩存中數據的有效性!!

這也是所有的ORM產品共同具有的問題。幸運的是,Hibernate爲我們暴露了Cache的清除方法,這給我們提供了一個手動保證數據有效性的機會!!

一級緩存,二級緩存都有相應的清除方法。
其中二級緩存提供的清除方法爲:
按對象class清空緩存
按對象class和對象的主鍵id清空緩存
清空對象的集合中的緩存數據等。
1.3.2. 適合使用的情況
並非所有的情況都適合於使用二級緩存,需要根據具體情況來決定。同時可以針對某一個持久化對象配置其具體的緩存策略。
適合於使用二級緩存的情況:
1、數據不會被第三方修改;
一般情況下,會被hibernate以外修改的數據最好不要配置二級緩存,以免引起不一致的數據。但是如果此數據因爲性能的原因需要被緩存,同時又有可能被第3方比如SQL修改,也可以爲其配置二級緩存。只是此時需要在sql執行修改後手動調用cache的清除方法。以保證數據的一致性
2、數據大小在可接收範圍之內;
如果數據表數據量特別巨大,此時不適合於二級緩存。原因是緩存的數據量過大可能會引起內存資源緊張,反而降低性能。
如果數據表數據量特別巨大,但是經常使用的往往只是較新的那部分數據。此時,也可爲其配置二級緩存。但是必須單獨配置其持久化類的緩存策略,比如最大緩存數、緩存過期時間等,將這些參數降低至一個合理的範圍(太高會引起內存資源緊張,太低了緩存的意義不大)。
3、數據更新頻率低;
對於數據更新頻率過高的數據,頻繁同步緩存中數據的代價可能和 查詢緩存中的數據從中獲得的好處相當,壞處益處相抵消。此時緩存的意義也不大。
4、非關鍵數據(不是財務數據等)
財務數據等是非常重要的數據,絕對不允許出現或使用無效的數據,所以此時爲了安全起見最好不要使用二級緩存。
因爲此時 “正確性”的重要性遠遠大於 “高性能”的重要性。

2. 目前系統中使用hibernate緩存的建議
1.4. 目前情況
一般系統中有三種情況會繞開hibernate執行數據庫操作:
1、多個應用系統同時訪問一個數據庫
此種情況使用hibernate二級緩存會不可避免的造成數據不一致的問題,
此時要進行詳細的設計。比如在設計上避免對同一數據表的同時的寫入操作,
使用數據庫各種級別的鎖定機制等。
2、動態表相關
所謂“動態表”是指在系統運行時根據用戶的操作系統自動建立的數據表。
比如“自定義表單”等屬於用戶自定義擴展開發性質的功能模塊,因爲此時數據表是運行時建立的,所以不能進行hibernate的映射。因此對它的操作只能是繞開hibernate的直接數據庫JDBC操作。
如果此時動態表中的數據沒有設計緩存,就不存在數據不一致的問題。
如果此時自行設計了緩存機制,則調用自己的緩存同步方法即可。
3、使用sql對hibernate持久化對象表進行批量刪除時
此時執行批量刪除後,緩存中會存在已被刪除的數據。
分析:
當執行了第3條(sql批量刪除)後,後續的查詢只可能是以下三種方式:
a. session.find()方法:
根據前面的總結,find方法不會查詢二級緩存的數據,而是直接查詢數據庫。
所以不存在數據有效性的問題。
b. 調用iterate方法執行條件查詢時:
根據iterate查詢方法的執行方式,其每次都會到數據庫中查詢滿足條件的id值,然後再根據此id 到緩存中獲取數據,當緩存中沒有此id的數據纔會執行數據庫查詢;
如果此記錄已被sql直接刪除,則iterate在執行id查詢時不會將此id查詢出來。所以,即便緩存中有此條記錄也不會被客戶獲得,也就不存在不一致的情況。(此情況經過測試驗證)
c. 用get或load方法按id執行查詢:
客觀上此時會查詢得到已過期的數據。但是又因爲系統中執行sql批量刪除一般是
針對中間關聯數據表,對於中間關聯表的查詢一般都是採用條件查詢 ,按id來查詢某一條關聯關係的機率很低,所以此問題也不存在!
如果某個值對象確實需要按id查詢一條關聯關係,同時又因爲數據量大使用 了sql執行批量刪除。當滿足此兩個條件時,爲了保證按id 的查詢得到正確的結果,可以使用手動清楚二級緩存中此對象的數據的方法!!
(此種情況出現的可能性較小)
1.5. 建議
1、建議不要使用sql直接執行數據持久化對象的數據的更新,但是可以執行 批量刪除。(系統中需要批量更新的地方也較少)
2、如果必須使用sql執行數據的更新,必須清空此對象的緩存數據。調用
SessionFactory.evict(class)
SessionFactory.evict(class,id)等方法。
3、在批量刪除數據量不大的時候可以直接採用hibernate的批量刪除,這樣就不存在繞開hibernate執行sql產生的緩存數據一致性的問題。
4、不推薦採用hibernate的批量刪除方法來刪除大批量的記錄數據。
原因是hibernate的批量刪除會執行1條查詢語句外加 滿足條件的n條刪除語句。而不是一次執行一條條件刪除語句!!
當待刪除的數據很多時會有很大的性能瓶頸!!!如果批量刪除數據量較大,比如超過50條,可以採用JDBC直接刪除。這樣作的好處是隻執行一條sql刪除語句,性能會有很大的改善。同時,緩存數據同步的問題,可以採用 hibernate清除二級緩存中的相關數據的方法。
調用 SessionFactory.evict(class) ;SessionFactory.evict(class,id)等方法。
所以說,對於一般的應用系統開發而言(不涉及到集羣,分佈式數據同步問題等),因爲只在中間關聯表執行批量刪除時調用了sql執行,同時中間關聯表一般是執行條件查詢不太可能執行按id查詢。所以,此時可以直接執行sql刪除,甚至不需要調用緩存的清除方法。這樣做不會導致以後配置了二級緩存引起數據有效性的問題。


退一步說,即使以後真的調用了按id查詢中間表對象的方法,也可以通過調用清除緩存的方法來解決。
4、具體的配置方法
根據我瞭解的很多hibernate的使用者在調用其相應方法時都迷信的相信“hibernate會自行爲我們處理性能的問題”,或者“hibernate 會自動爲我們的所有操作調用緩存”,實際的情況是hibernate雖然爲我們提供了很好的緩存機制和擴展緩存框架的支持,但是必須經過正確的調用其纔有可能發揮作用!!所以造成很多使用hibernate的系統的性能問題,實際上並不是hibernate不行或者不好,而是因爲使用者沒有正確的瞭解其使用方法造成的。相反,如果配置得當hibernate的性能表現會讓你有相當“驚喜的”發現。下面我講解具體的配置方法.
ibernate提供了二級緩存的接口:
net.sf.hibernate.cache.Provider,
同時提供了一個默認的 實現net.sf.hibernate.cache.HashtableCacheProvider,
也可以配置 其他的實現 比如ehcache,jbosscache等。
具體的配置位置位於hibernate.cfg.xml文件中
<property >true</property>
<property >net.sf.hibernate.cache.HashtableCacheProvider</property>
很多的hibernate使用者在 配置到 這一步 就以爲 完事了,
注意:其實光這樣配,根本就沒有使用hibernate的二級緩存。同時因爲他們在使用hibernate時大多時候是馬上關閉session,所以,一級緩存也沒有起到任何作用。結果就是沒有使用任何緩存,所有的hibernate操作都是直接操作的數據庫!!性能可以想見。
正確的辦法是除了以上的配置外還應該配置每一個vo對象的具體緩存策略,在影射文件中配置。例如:
<hibernate-mapping>
<class table="dcm_datatype">
<cache usage="read-write"/>
<id column="TYPEID" type="java.lang.Long">
<generator />
</id>
<property column="NAME" type="java.lang.String"/>
<property column="DBTYPE" type="java.lang.String"/>
</class>
</hibernate-mapping>
關鍵就是這個<cache usage="read-write"/>,其有幾個選擇
read-only,read-write,transactional,等
然後在執行查詢時 注意了 ,如果是條件查詢,或者返回所有結果的查詢,此時session.find()方法 不會獲取緩存中的數據。只有調用query.iterate()方法時纔會調緩存的數據。
同時 get 和 load方法 是都會查詢緩存中的數據 .
對於不同的緩存框架具體的配置方法會有不同,但是大體是以上的配置
(另外,對於支持事務型,以及支持集羣的環境的配置我會爭取在後續的文章中中 發表出來)

3.總結
總之是根據不同的業務情況和項目情況對hibernate進行有效的配置和正確的使用,揚長避短。不存在適合於任何情況的一個“萬能”的方案。
Hibernate數據緩存
Hibernate緩存是一種提高系統性能的比較好的工具,如果使用合理,則能極大地提高系統性能,但如果使用不合理也會使用系統性能下降。

Hibernate緩存分類:
Hibernate緩存我們通常分兩類,一類稱爲一級緩存也叫內部緩存,另一類稱爲二級緩存。Hibernate的這兩級緩存都位於持久化層,存放的都是數據庫數據的拷貝,那麼它們之間的區別是什麼呢?爲了理解二者的區別,需要深入理解持久化層的緩存的一個特性:緩存的範圍。
緩存的範圍決定了緩存的生命週期以及可以被誰訪問。緩存的範圍分爲三類。
(1) 事務範圍:緩存只能被當前事務訪問。緩存的生命週期依靠於事務的生命週期,當事務結束時,緩存也就結束生命週期。在此範圍下,緩存的介質是內存。事務可以是數據庫事務或者應用事務,每個事務都有獨自的緩存,緩存內的數據通常採用相互關聯的的對象形式, 一級緩存就屬於事務範圍。
(2) 應用範圍:緩存被應用範圍內的所有事務共享。這些事務有可能是併發訪問緩存,因此必須對緩存採取必要的事務隔離機制。緩存的生命週期依靠於應用的生命週期,應用結束時,緩存也就結束了生命週期,二級緩存存在於應用範圍。
(3) 集羣範圍:在集羣環境中,緩存被一個機器或者多個機器的進程共享。緩存中的數據被複制到集羣環境中的每個進程節點,進程間通過遠程通信來保證緩存中的數據的一致性,緩存中的數據通常採用對象的鬆散數據形式,二級緩存也存在與應用範圍。
注重:對大多數應用來說,應該慎重地考慮是否需要使用集羣範圍的緩存,因爲訪問它的速度不一定會比直接訪問數據庫數據的速度快多少,再加上集羣範圍還有數據同步的問題,所以應當慎用。
持久化層可以提供多種範圍的緩存。假如在事務範圍的緩存中沒有查到相應的數據,還可以到應用範圍或集羣範圍的緩存內查詢,假如還是沒有查到,那麼只有到數據庫中查詢了。

Session緩存(一級緩存):
當調用Session的保存、更新、查詢操作時,在Session緩存中不存在相應對象,則把這些對象加入Session緩存。同一個Session操作,第一次通過ID調用load()或get()查詢持久對象,先從Session緩存中查詢發現該對象不命中,隨即發送一條SQL語句生成一個持久對象並把該對象放入Session緩存。第二次再通過相同ID調用load()或get()查詢時將直接從Session緩存將該對象返回,避免多餘的數據庫連接和查詢的開銷。
Session的load()和get()方法使用區別:
1、當數據庫不存在對應ID數據時,調用load()方法將會拋出ObjectNotFoundException異常,get()方法將返回null。
2、當對象.hbm.xml配置文件<class>元素的lazy屬性設置爲true時(延遲加載),調用load()方法時則返回持久對象的代理類實例,此時的代理類實例是由運行時代理動態生成的類,該代理類實例包括原目標對象的所有屬性和方法,該代理類實例的屬性除了ID不爲null外,所在屬性爲null值,查看日誌並沒有Hibernate SQL輸出,說明沒有執行查詢操作,當代理類實例通過getXXX()方法獲取屬性值時,Hiberante才真正執行數據庫查詢操作。當對象.hbm.xml配置文件<class>元素的lazy屬性設置爲false時,調用load()方法則是立即執行數據庫並直接返回實體類,並不返回代理類。而調用get()方法時不管lazy爲何值,都直接返回實體類。
3、load()和get()都會先從Session緩存中查找,如果沒有找到對應的對象,則查詢Hibernate二級緩存,再找不到該對象,則發送一條SQL語句查詢。
Session的evict()方法將持久對象從Session緩存中清除,clear()方法將清空整個緩存。

二級緩存(SesionFactory):
二級緩存由SessionFactory創建的所有Session對象共享使用,我們什麼情況下要使用二級緩存?假如滿足以下條件,則可以將其納入二級緩存:
(1)數據不會被第三放修改
(2)同一數據系統經常引用
(3)數據大小在可接受範圍之內
(4)非要害數據,或不會被併發的數據
Hibernate本身並不提供二級緩存的產品化實現,而是爲衆多支持Hibernate的第三方緩存組件提供整和接口。
現在主流的EHCache,它更具備良好的調度性能。
配置:在hibernate中啓動二級類緩存,需要在hibernate.cfg.xml配置以下參數:
<hibernate-configuration>
<session-factory>
……
<property > org.hibernate.cache.EhCacheProvider
<./property>
</session-factory>
</hibernate-configuration>
另外還需要對ehcache.xml進行配置,這是一個單獨的xml文件,示例如下:
ehcache.xml
<ehcache>
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="10000"
timeToLiveSeconds="10000"
overflowToDisk="true"
/>
<cache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="10000"
timeToLiveSeconds="10000"
overflowToDisk="true"
/>
</ehcache>
<diskStore>表示當內存緩存中對象數量超過類設置內存緩存數量時,將緩存對象寫到硬盤,path=”java.io.tmpdir”表示把數據寫到這個目錄下。Java.io.tmpdir目錄在運行時會根據相對路徑生成。
<defaultCache>表示設定緩存的默認數據過期策略。
<cache>表示設定用具體的命名緩存的數據過期策略。
name表示具體的緩存命名。
maxElementsInMemory表示cache中最大允許保存的對象數據量。
eternal表示cache中數據是否爲常量。
timeToIdleSeconds表示緩存數據鈍化時間
timeToLiveSeconds表示緩存數據的生命時間。
overflowToDisk表示內存不足時,是否啓用磁盤緩存。

Hibernate提供了四種緩存同步策略:
(1)read-only策略:只讀,對於數據庫表的數據不會改變的數據,可以使用只讀型緩存。例如城市表的數據不會發生變化,則可配置類緩存爲:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class table="tbl_city" lazy="false" mutable="false">
<cache usage="read-only" />
<id type="java.lang.Integer">
<column />
<generator ></generator>
</id>
<property type="java.lang.String">
<column />
</property>
<property type="java.lang.String">
<column />
</property>
<property type="java.lang.Integer">
<column />
</property>
</class>
</hibernate-mapping>
(2)nonstrict-read-write
不嚴格可讀寫緩存。假如應用程序對併發訪問下的數據同步要求不是很嚴格的話,而且數據更新操作頻率較低。採用本項,可獲得良好的性能。
(3) read-write
對於經常被讀但很少修改的數據,可以採用這種隔離類型,因爲它可以防止髒讀這類的併發問題.
(4)transactional(事物型)
在Hibernate中,事務型緩存必須運行在JTA事務環境中。

查詢緩存:我們前面提到查詢緩存(Query Cache)依靠二級緩存,這到底是怎麼回事呢?我看看二級緩存策略的一般過程:
(1) Hibernate進行條件查詢的時候,總是發出一條select * from XXX where …(XXX爲 表名,類似的語句下文統稱Select SQL)這樣的SQL語句查詢數據庫,一次獲得所有的符合條件的數據對象。
(2) 把獲得的所有數據對象根據ID放入到第二級緩存中。
(3) 當Hibernate根據ID訪問數據對象的時候,首先從內部緩存中查找,假如在內部緩存中查不到就配置二級緩存,從二級緩存中查;假如還查不到,再查詢數據庫,把結果按照ID放入到緩存。
(4)添加數據、刪除、更新操作時,同時更新二級緩存。這就是Hibernate做批處理的時候效率不高的原因,原來是要維護二級緩存消耗大量時間的緣故。
我們看到這個過程後,可以明顯的發現什麼?那就是Hibernate的二級緩存策略是針對ID查詢的策略,和對象ID密切相關,那麼對於條件查詢就怎麼適用了。對於這種情況的存在,Hibernate引入了“查詢緩存”在一定程度上緩解這個問題。
那麼我們先來看看我們爲什麼使用查詢緩存?首先我們來思考一個問題,假如我們對數據表Student進行查詢操作,查找age>20的所有學生信息,然後納入二級緩存;第二次我們的查詢條件變了,查找age>15的所有學生信息,顯然第一次查詢的結果完全滿足第二次查詢的條件,但並不是滿足條件的全部數據。這樣的話,我們就要再做一次查詢得到全部數據才行。再想想,假如我們執行的是相同的條件語句,那麼是不是可以利用之前的結果集呢?

Hibernate就是爲了解決這個問題的而引入Query Cache的。

查詢緩存策略的一般過程如下:
(1)Query Cache保存了之前查詢的執行過的Select SQL,以及結果集等信息,組成一個Query Key。(2)當再次碰到查詢請求的時候,就會根據Query Key 從Query Cache找,找到就返回。但 是兩次查詢之間,數據表發生數據變動的話,Hibernate就會自動清除Query Cache中對應的Query Key。
我們從查詢緩存的策略中可以看出,Query Cache只是在特定的條件下才會發揮作用,而且要求相當嚴格:
(1)完全相同的Select SQL重複執行。
(2)重複執行期間,Query Key對應的數據表不能有數據變動(比如添、刪、改操作)
爲了啓用Query Cache,我們需要在hibernate.cfg.xml中進行配置,參考配置如下(只列出核心配置項):
<hibernate-configuration>
<session-factory> …………
<property ………… </session-factory>
</hibernate-configuration>
應用程序中必須在查詢執行之前,將Query.Cacheable設置爲true,而且每次都應該這樣。比如:
Query query=session.createQuery(hql).setInteger(0.15); query.setCacheable(true); ………
在Hibernate中,緩存將在以下情況中發揮作用:
1.通過id[主鍵]加載數據的時候
2.延遲加載

一級緩存:
又稱內部緩存,保存了與當前session相關聯的數據對象,伴隨Session實例的創建而創建,消亡而消亡。因此又稱此緩存爲Session level cache。
一級緩存正常情況下又Hibernate自動維護,如果需要手動干預,可以通過以下方法完成。
1.Session.evict
將某個特定對象從內部緩存中清除。
2.Sessin.clear
清空內部緩存

二級緩存:
又稱爲SessionFactory Level Cache.
對什麼樣的數據使用二級緩存?對所有數據都進行緩存是最簡單的辦法,也是最常用的辦法。但是某些情況下,反而會影響性能,比如電話費查詢系統,如果實行緩存,內存會被幾乎不可能再被重用的數據充斥,導致性能下降。
如果數據滿足以下條件,可以將其納入緩存管理:
1.數據不會被第三方應用修改。
2.data size在可以接受的範圍之內
3.數據更新頻率較低
4.同一數據可能會被系統頻繁引用
5.非關鍵數據
Hibernate本身並沒提供二級緩存的產品化實現(只提供了一個基於HashTable的簡單緩存以供調試),可以使用第三方緩存來實現。默認採用EHCache作爲二級緩存實現。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章