Hibernate學習筆記1 本筆記學習自 "Hibernate官方文檔" 。 1. Architecture(總體結構) 俗話說有圖有真相,先看一張圖: 上圖清楚地描述了Hibernate在一個Java應用中所處的位置:位於 Data Access Layer(數據訪問層) 和 Relation ...
Hibernate學習筆記1
本筆記學習自Hibernate官方文檔。
1. Architecture(總體結構)
俗話說有圖有真相,先看一張圖:
上圖清楚地描述了Hibernate在一個Java應用中所處的位置:位於Data Access Layer(數據訪問層)和Relation Database(關係型資料庫)之間。最原始地,我們在數據訪問層中是通過JDBC去操作資料庫的,但JDBC編程起來非常繁瑣,並且需要我們手動去處理資料庫中每條數據與其對應對象的轉換(即所謂的ORM,Object Relational Mapping,對象關係映射),Hibernate很好地解決了以上問題。Hibernate實現了JPA(Java Persistence API,Java官方給出的持久化API),因此我們可以使用JPA去使用Hibernate,當然也可以使用Hibernate自己的(原生的)API。
再看一個圖,這是我們常用的Hibernate的介面(類)的繼承體繫結構圖,以後再作介紹,先有個大致的印象:
2. Domain Model(領域模型)
Domain Model(領域模型)是一個抽象的系統,它描述了我們關註的問題所包含的知識、所產生的影響或活動的範圍,該模型可以幫助我們解決與該領域相關的問題。
對編程而言,我們通常編寫的POJO/JavaBean便是領域模型(或者更具體地叫做anemic domain model,貧血領域模型),這是Hibernate的中心角色。
2.1 快速入門
概念講了一大堆,我想你更加關心的是這東西到底咋用啊。下麵來一個入門例子。例子很簡單,我們有一個User(用戶)POJO,我們要對其進行CURD(增刪改查)操作。User數據結構如下:
public class User {
private Long id;
private String name;
// setter、getter方法略
}
首先創建一個Maven項目(強烈建議使用Maven,Gradle等項目構建工具來開發,不要再到處找jar包導入了),然後在pom.xml中引入Hibernate和一些必要的依賴,如下:
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<!--如果使用Hibernate原生API,使用hibernate-core代替hibernate-entitymanager -->
<artifactId>hibernate-entitymanager</artifactId>
<version>5.2.8.Final</version>
</dependency>
<!--mysql驅動-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.3</version>
</dependency>
<!--測試框架-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
依賴搞定,在這例子中我們使用JPA,現在配置Hibernate。在resources目錄(如果不是Maven項目就在類的根目錄)下新建META-INF文件夾,再在META-INF下新建配置文件persistence.xml,這個配置文件到時候會自動被Hibernate讀取。
persistence.xml
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="cn.derker">
<class>cn.derker.model.User</class>
<properties>
<property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=UTF-8"/>
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="javax.persistence.jdbc.password" value="root"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
</properties>
</persistence-unit>
</persistence>
配置項根據名稱應該能大致猜出是什麼意思。可以看出我測試的是MySQL資料庫。解釋一下幾個配置的意思:persistence-unit
的name
屬性的值是自己定義的,但之後在Java代碼中會用到。class
標簽的內容是需要持久化的對象的全類名;hibernate.hbm2ddl.auto
配置項的取值和含義如下:
- create:啟動的時候先drop定義的持久化類對應的表,再重新create,這樣表中的數據全沒了,慎用!!!
- create-drop: 啟動時create表,關閉時drop表;
- update: 啟動時會去檢查各個表的schema是否一致,如果不一致會做更新表結構
- validate: 啟動時驗證各個表的schema與在hibernate中定義的持久化類數據結構信息是否一致。如果不一致就拋出異常,並不做更新。
在以上配置中,我們告訴了hibernate去連接哪一個資料庫,包括連接資料庫的用戶名,密碼,以及哪些POJO類需要藉助hibernate,但我們沒有告訴hibernate如何去定義這些POJO類對應的表結構。hibernate提供了兩種方式定義(映射):一種是xml配置;另一種是註解配置。鑒於xml配置不如註解配置方便,下麵的例子一律採用註解配置。怎麼將User
類的結構描述給hibernate呢?如下:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public User() {
}
public User(String name) {
this.name = name;
}
// setter、getter方法略
}
首先是在類的名稱上聲明瞭@Entity
註解;然後在id
欄位上聲明瞭@Id
和@GeneratedValue(strategy = GenerationType.IDENTITY)
。其中@Id
表明把id
欄位定義為主鍵;@GeneratedValue
說明瞭主鍵的生成策略是自動增長的(當然,這需要你使用的資料庫支持這一特性)。這樣所有的配置就搞定了。
接下來就可以測試CURD操作了,直接貼出測試類的代碼:
public class TestApp {
private EntityManagerFactory entityManagerFactory;
@Before
public void init() {
// cn.derker與上面配置的persistence.xml中persistence-unit的name值一致
entityManagerFactory = Persistence.createEntityManagerFactory("cn.derker");
}
@After
public void destroy() throws Exception {
entityManagerFactory.close();
}
@Test
public void testSave() throws Exception {
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
entityManager.persist(new User("張三")); // 為User添加了一個有參的構造方法,但千萬別忘了聲明一個無參的構造方法
entityManager.getTransaction().commit();
}
@Test
public void testFind() throws Exception {
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
User user = entityManager.find(User.class, 1L);
Assert.assertEquals("張三", user.getName());
entityManager.getTransaction().commit();
}
@Test
public void testUpdate() throws Exception {
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
User user = entityManager.find(User.class, 1L);
user.setName("李四"); // 這裡不必再調用update方法,hibernate會自動更新
entityManager.getTransaction().commit();
}
@Test
public void testDelete() throws Exception {
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
User user = entityManager.find(User.class, 1L);
entityManager.remove(user);
entityManager.getTransaction().commit();
}
}
2.2 映射類型
Hibernate將數據的類型分為兩大類:value types和entity types。value types類型的數據依附於entity typed類型的數據。舉個慄子:
class User{
Long id;
String name;
Address address;
}
像User
便是entity type,它有自己的生命周期;而想Long、String類型的id、name包括Address類型的address都是value type,它們不會單獨存在,而是依附於User。
顯然,value type就包括3種類型的數據,一種是basic types,比如一些表示數值、表示字元、表示日期的類型等(見這裡);第二種是embeddable types,像上面例子中的Address
類型;最後還有一種是collection types,這很明顯。
關於basic types和embeddable types之後小節會詳細介紹。
2.3 命名策略
如果你運行了上面入門中的慄子,你會發現Hibernate預設幫我們建立的表的表名和列名與我們在類中定義的名稱是一致的,這是Hibernate的預設命名策略。如果我們想自定義表名或者列名,有兩種方法可以實現。
一種是被稱為ImplicitNamingStrategy的方案。做法很簡單,只需要使用@Table
和@Column
註解即可,其中name
屬性的值就是表或列的名字,慄子如下:
@Entity
@Table(name = "tb_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "col_name")
private String name;
...
}
另一種方案叫做PhysicalNamingStrategy,這種方案可以進行“大批量”地命名。比如有些人習慣將表名命名成"tb_" + 蛇形命名的類名
,將列名命名為"col_" + 蛇形命名的欄位名
,如果採用上一種命名策略方案將會寫大量的註解,若採用該命名策略方案則只需定義一個類即可,該類需要實現PhysicalNamingStrategy
介面,慄子如下:
public class MyNamingStrategy implements PhysicalNamingStrategy {
@Override
public Identifier toPhysicalCatalogName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return name;
}
@Override
public Identifier toPhysicalSchemaName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return name;
}
@Override
public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return jdbcEnvironment.getIdentifierHelper().toIdentifier("tb_" + camel2Snake(name.getText()));
}
@Override
public Identifier toPhysicalSequenceName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return name;
}
@Override
public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment jdbcEnvironment) {
if ("id".equals(name.getText())) {
return name;
}
return jdbcEnvironment.getIdentifierHelper().toIdentifier("col_" + camel2Snake(name.getText()));
}
/**
* 駝峰命名轉蛇形命名
*/
private String camel2Snake(String name) {
StringBuilder sb = new StringBuilder(name.length() * 2);
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if(c == ' '){
sb.append('_');
continue;
}
if (Character.isUpperCase(c)) {
if (i != 0) {
sb.append('_');
}
sb.append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString();
}
}
以上代碼主要是在toPhysicalTableName
和toPhysicalColumnName
方法中做了“手腳”,它們的返回值分別決定了表名和列名。
定義了MyNamingStrategy
後還需要將它“告訴”Hibernate,很簡單,只需要在配置文件persistence.xml中加上一個配置參數就搞定了:
<properties>
<property name="hibernate.physical_naming_strategy" value="cn.derker.strategy.MyNamingStrategy"/>
</properties>
2.4 Basic Types
嚴格地講,basic types 類型的數據必須要使用@Basic註解,但是我們之前說過,basic types 類型包括表示數值,表示字元,表示時間等數據類型,這是我們經常大量定義的類型,因此@Basic可以省略不寫,它預設被假定。一個嚴格的User
:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic
private Long id;
@Basic
private String name;
...
}
事實上,@Basic
註解還有兩個屬性:
- optional - boolean (defaults to true):定義是否允許值為
null
。 - fetch - FetchType (defaults to EAGER):定義是否採用懶載入策略。
FetchType.EAGER
表示不使用;FetchType.LAZY
表示使用。事實上,對於basic type,Hibernate實現的時候忽略了該屬性。
我們知道Java的基本數據類型和Sql的數據類型不是一一對應的,存在著一些差別,而在上面的慄子中我們並沒有“告訴”Hibernate如何去處理這些差異帶來的問題,Hibernate是如何知道怎麼將Java的某種基本數據類型轉換成Sql數據類型呢?其實在Hibernate中已經定義好了許多這樣實現數據類型轉換功能的類,比如針對java.lang.String
的org.hibernate.type.StringType
,處理java.lang.Integer
的org.hibernate.type.IntegerType
。這啟示我們,可以寫一個類似這樣的類,去處理一些Hibernate還不支持的Java類型的轉換,比如下麵的慄子,自定義一個basic type,可以將String[]
類型的數據以Json的字元串的形式保存在資料庫中,並且在讀取時自動還原成之前的字元數組。
為此,我們為之前的User
對象添加一個hobbies
(愛好)屬性:
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Type(type = "String[]")
private String[] hobbies;
public User() {
}
public User(String name, String[] hobbies) {
this.name = name;
this.hobbies = hobbies;
}
// 省略setter、getter、toString方法
}
註意,我們為String[] hobbies
加上了org.hibernate.annotations.Type
註解,並且指明瞭需要使用名叫“String[]”的Hibernate基本類型去處理。這個"String[]"命名是隨意的,但要與我們定義時的名字一致。下麵來定義這個basic type:
public class StringArrayType extends AbstractSingleColumnStandardBasicType<String[]>
implements DiscriminatorType<String[]> {
public static final StringArrayType INSTANCE = new StringArrayType();
public StringArrayType() {
super(VarcharTypeDescriptor.INSTANCE, StringArrayTypeDescriptor.INSTANCE);
}
@Override
public String[] stringToObject(String xml) throws Exception {
return fromString( xml );
}
@Override
public String objectToSQLString(String[] value, Dialect dialect) throws Exception {
return toString(value);
}
@Override
public String getName() {
return "String[]";
}
}
觀察以上代碼,我們定義了一個StringArrayType
類,它繼承自AbstractSingleColumnStandardBasicType,實現了DiscriminatorType
介面,泛型參數類型為我們要處理的Java類型String[]
,並且該類還採用了單例模式(餓漢式),這是自定義基本類型的“標準做法”。在構造方法中,調用了父類的構造方法,兩個參數分別是SqlTypeDescriptor(Sql類型描述符)和JavaTypeDescriptor(描述符),代表需要在Sql的varchar類型和Java的String[]類型間做轉換。接下去實現了三個方法,其中stringToObject
方法是用來指明如何將discriminator-value
(這個以後會說明)的值轉換為我們的需要處理的Java類型;objectToSQLString
方法用來指明如何將我們需要處理的類型的對象轉化為Sql語句(片段)。這兩個方法都是直接調用了父類的fromString
和toString
方法,事實上在父類中,這兩個方法調用了我們之前在構造函數中給定的 JavaTypeDescriptor的fromString
和toString
方法;最後還實現的一個方法是getName
,該方法用來定義該類型的名稱,即之前我們在Type
註釋中使用的。
接下來需要實現上面使用的JavaTypeDescriptor——StringArrayTypeDescriptor:
public class StringArrayTypeDescriptor extends AbstractTypeDescriptor<String[]> {
public static final StringArrayTypeDescriptor INSTANCE = new StringArrayTypeDescriptor();
protected StringArrayTypeDescriptor() {
super(String[].class);
}
@Override
public String toString(String[] value) {
return JSON.toJSONString(value);
}
@Override
public String[] fromString(String string) {
JSONArray jsonArray = JSON.parseArray(string);
String[] result = new String[jsonArray.size()];
for (int i = 0; i < result.length; i++) {
result[i] = jsonArray.getString(i);
}
return result;
}
@Override
@SuppressWarnings({"unchecked"})
public <X> X unwrap(String[] value, Class<X> type, WrapperOptions options) {
if (value == null) {
return null;
}
if (type != String.class) {
throw unknownUnwrap(type);
}
return (X) toString(value);
}
@Override
public <X> String[] wrap(X value, WrapperOptions options) {
if (!(value instanceof String)) {
throw unknownWrap(value.getClass());
}
return fromString((String) value);
}
}
該類也採用單例模式,需要實現toString
,fromString
,unwrap
,wrap
方法。toString
和fromString
方法指定我們需要處理的類與String之間如何轉換,這也是上面提到的被調用的JavaTypeDescriptor的toString
和fromString
方法;unwrap
方法在為String[]類型欄位指定PreparedStatement
的參數時調用;wrap
方法相反,是表明如何將從資料庫中取出的值轉換為需要處理的Java類型。
在其中,我們用到了阿裡的fastjson庫,需要在Maven的pom.xml中添加依賴:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
最後還需要將我們定義的這個基本類型“告訴”hibernate。“告訴”的方式官方給出了兩種,但都是用Hibernate原生的API調用的,我暫時還沒有找到用JPA怎麼添加基本類型的,如果你知道,請告訴我。
這裡就採用原生的API來舉例吧。我們需要添加新的配置文件hibernate.cfg.xml,該文件位於resources目錄或類的根路徑下,內容和之前JPA使用的persistence.xml類似,如下:
<?xml version='1.0' encoding='utf-8'?>
<!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>
<property name="connection.driver_class">com.mysql.cj.jdbc.Driver</property>
<property name="connection.url">jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=UTF-8
</property>
<property name="connection.username">root</property>
<property name="connection.password">root</property>
<property name="connection.pool_size">5</property>
<property name="dialect">org.hibernate.dialect.MySQL57InnoDBDialect</property>
<property name="cache.provider_class">org.hibernate.cache.internal.NoCacheProvider</property>
<property name="show_sql">true</property>
<property name="hbm2ddl.auto">update</property>
<property name="hibernate.physical_naming_strategy">cn.derker.strategy.MyNamingStrategy</property>
<mapping class="cn.derker.model.User"/>
</session-factory>
</hibernate-configuration>
之後在Java代碼中我們這樣將定義的StringArrayType
添加到hibernate中:
@Before
public void init() {
final StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.configure() // configures settings from hibernate.cfg.xml
.build();
MetadataSources sources = new MetadataSources(registry);
MetadataBuilder metadataBuilder = sources.getMetadataBuilder();
metadataBuilder.applyBasicType(StringArrayType.INSTANCE);
sessionFactory = metadataBuilder.build().buildSessionFactory();
}
hibernate原生API的sessionFactory
與JPA的EntityManager
類似,有了它之後,可以這樣調用添加一個用戶:
@Test
public void testSave() throws Exception {
Session session = sessionFactory.openSession();
session.getTransaction().begin();
User user = new User("張三", new String[]{"吃飯", "睡覺", "打豆豆"});
session.persist(user);
session.getTransaction().commit();
session.close();
}
這樣就可以將一個Java字元數組類型的欄位以json字元的形式(sql中是varchar類型)保存在資料庫中。其他更新、查找、刪除方法就不一一給出了,總之都是通過sessionFactory.openSession()
得到一個Session
對象,然後調用session對想的相關方法。Session
對象與JPA中的EntityManager
是類似的。
3. 小結
以上內容除了對hibernate的一些基本概念的介紹外,主要給出了三個問題的解答:
- 如何使用Hibernate對一個對象進行最基本的CURD操作;
- 如何自定義表名,列名;
- 如何自定義Hibernate基本類型處理“非一般”Java類型,如處理String[]類型;