1、爲什麼需要hibernate
- 簡單介紹jdbc
我們學習Java數據庫操作時,一般會設計到jdbc的操作,這是一位程序員最基本的素養。jdbc以其優美的代碼和高性能,將瞬時態的javabean對象轉化爲持久態的SQL數據。但是,每次SQL操作都需要建立和關閉連接,這勢必會消耗大量的資源開銷;如果我們自行創建連接池,假如每個項目都這樣做,勢必搞死的了。同時,我們將SQL語句預編譯在PreparedStatement中,這個類可以使用佔位符,避免SQL注入,當然,後面說到的hibernate的佔位符的原理也是這樣,同時,mybatis的#{}佔位符原理也是如此。預編譯的語句是原生的SQL語句,比如更新語句:
private static int update(Student student) {
Connection conn = getConn();
int i = 0;
String sql = "update students set Age='" + student.getAge() + "' where Name='" + student.getName() + "'";
PreparedStatement pstmt;
try {
pstmt = (PreparedStatement) conn.prepareStatement(sql);
i = pstmt.executeUpdate();
System.out.println("resutl: " + i);
pstmt.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
return i;
}
上面的sql語句沒有使用佔位符,如果我們少了varchar類型的單引號,就會保存失敗。在這種情況下,如果寫了很多句代碼,最後因爲一個單引號,導致代碼失敗,對於程序員來說,無疑是很傷自信心的事。如果涉及到了事務,那麼就會非常的麻煩,一旦一個原子操作的語句出現錯誤,那麼事務就不會提交,自信心就會跌倒低谷。然而,這僅僅是更新語句,如果是多表聯合查詢語句,那麼就要寫很多代碼了。具體的jdbc的操作,可以參考這篇文章:jdbc操作。
因而,我們在肯定它的優點時,也不應該規避他的缺點。隨着工業化步伐的推進,每個數據庫往往涉及到很多表,每張表有可能會關聯到其他表,如果我們還是按照jdbc的方式操作,這無疑是增加了開發效率。所以,有人厭倦這種複雜繁瑣、效率低下的操作,於是,寫出了著名的hibernate框架,封裝了底層的jdbc操作,以下是jdbc的優缺點:
由上圖可以看見,jdbc不適合公司的開發,公司畢竟以最少的開發成本來創造更多的利益。這就出現了痛點,商機伴隨着痛點的出現。因而,應世而生了hibernate這個框架。即便沒有hibernate的框架,也會有其他框架生成。hibernate的底層封裝了jdbc,比如說jdbc爲了防止sql注入,一般會有佔位符,hibernate也會有響應的佔位符。hibernate是orm(object relational mapping)的一種,即對象關係映射。
- 什麼對象關係映射
通俗地來說,對象在pojo中可以指Javabean,關係可以指MySQL的關係型數據庫的表字段與javabean對象屬性的關係。映射可以用我們高中所學的函數映射,即Javabean順時態的對象映射到數據庫的持久態的數據對象。我們都知道javabean在內存中的數據是瞬時狀態或遊離狀態,就像是宇宙星空中的一顆顆行星,除非行星被其他行星所吸引,纔有可能不成爲遊離的行星。瞬時狀態(遊離狀態)的javabean對象的生命週期隨着進程的關閉或者方法的結束而結束。如果當前javabean對象與gcRoots沒有直接或間接的關係,其有可能會被gc回收。我們就沒辦法長久地存儲數據,這是一個非常頭疼的問題。假如我們使用文件來存儲數據,但是文件操作起來非常麻煩,而且數據格式不是很整潔,不利於後期的維護等。因而,橫空出世了數據庫。我們可以使用數據庫存儲數據,數據庫中的數據纔是持久數據(數據庫的持久性),除非人爲的刪除。這裏有個問題——怎麼將瞬時狀態(遊離狀態)的數據轉化爲持久狀態的數據,肯定需要一個連接Javabean和數據庫的橋樑,於是乎就有了上面的jdbc。
單獨來說mysql,mysql是一個遠程服務器。我們在向mysql傳輸數據時,並不是對象的方式傳輸,而是以字節碼的方式傳輸數據。爲了保證數據準確的傳輸,我們一般會序列化當前對象,用序列號標誌這個唯一的對象。如果,我們不想存儲某個屬性,它是有數據庫中的數據拼接而成的,我們大可不用序列化這個屬性,可以使用Transient來修飾。比如下面的獲取圖片的路徑,其實就是服務器圖片的文件夾地址和圖片的名稱拼接而成的。當然,你想存儲這個屬性,也可以存儲這個屬性。我們有時候圖片的路由的字節很長,這樣會佔用MySQL的內存。因而,學會取捨,未嘗不是一個明智之舉。
@Entity
@Table(name = "core_picture")
public class Picture extends BaseTenantObj {
。。。。
@Transient
public String getLocaleUrl() {
return relativeFolder.endsWith("/") ? relativeFolder + name : relativeFolder + "/" + name;
}
。。。。
}
網上流傳盛廣的對象關係映射的框架(orm)有hibernate、mybatis等。重點說說hibernate和mybatis吧。hibernate是墨爾本的一位厭倦重複的javabean的程序員編寫而成的,mybatis是appache旗下的一個子產品,其都是封裝了jdbc底層操作的orm框架。但hibernate更注重javabean與數據表之間的關係,比如我們可以使用註解生成數據表,也可以通過註解的方式設置字段的類型、註釋等。他將javabean分成了遊離態、順時態、持久態等,hibernate根據這三種狀態來觸及javabean對象的數據。而mybatis更多的是注重SQL語句的書寫,也就是說主要是pojo與SQL語句的數據交互,對此,Hibernate對查詢對象有着良好的管理機制,用戶無需關心SQL。一旦SQL語句的移動,有可能會影響字段的不對應,因而,mybatis移植性沒有hibernate好。mybatis接觸的是底層SQL數據的書寫,hibernate根據javabean的參數來生成SQL語句,再將SQL查詢的結果封裝成pojo,因而,mybatis的性能相來說優於hibernate,但這也不是絕對的。性能還要根據你的表的設計結構、SQL語句的封裝、網絡、帶寬等等。我只是拋磚引玉,它們具體的區別,可以參考這篇文檔。mybatis和hibernate的優缺點。
hibernate和mybatis之間的區別,也是很多公司提問面試者的問題。但是真正熟知他們區別的人,一般是技術選型的架構師。如果你只是負責開發,不需要了解它們的區別,因爲他們都封裝了jdbc。所以,你不論使用誰,都還是比較容易的。然而,很多公司的HR提問這種問題很死板,HR並不懂技術,他們只是照本宣科的提問。如果你照本宣科的回答,它們覺着你很厲害。但是,如果是一個懂技術的人提問你,如果你只是臨時背了它們的區別,而沒有相應的工作經驗,他們會問的讓你手足無措。
- hibernate的講解
因爲我們公司使用的是hibernate,我在這裏簡單地介紹下hibernate。但相對於jdbc來說,hibernate框架還是比較重的。爲什麼說他重,因爲它集成了太多的東西,看如下的hibernate架構圖:
你會發現最上層使我們的java應用程序的開始,比如web的Tomcat服務器的啓動,比如main方法的啓動等。緊接着就是需要(needing)持久化的對象,這裏爲什麼說是需要,而不是持久化的對象。只有保存到文件、數據庫中的數據纔是持久化的想通過hibernate,我們可以毫不費力的將瞬時狀態的數據轉化爲持久狀態的數據,下面便是hibernate的內部操作數據。其一般是這樣的流程:
- 我個人畫的
- 這個地址的圖片
如果你是用過jdbc連接數據庫的話,我們一般是這樣寫的:
package com.zby.jdbc.config;
import com.zby.util.exception.TableException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
/**
* Created By zby on 22:12 2019/1/5
*/
public class InitJdbcFactory {
private static Properties properties = new Properties();
private static Logger logger = LoggerFactory.getLogger(InitJdbcFactory.class);
static {
try {
//因爲使用的類加載器獲取配置文件,因而,配置文件需要放在classpath下面,
// 方能讀到數據
properties.load(Thread.currentThread().getContextClassLoader().
getResourceAsStream("./jdbc.properties"));
} catch (IOException e) {
logger.info("初始化jdbc失敗");
e.printStackTrace();
}
}
public static Connection createConnection() {
String drivers = properties.getProperty("jdbc.driver");
if (StringUtils.isBlank(drivers)) {
drivers = "com.mysql.jdbc.Driver";
}
String url = properties.getProperty("jdbc.url");
String username = properties.getProperty("jdbc.username");
String password = properties.getProperty("jdbc.password");
try {
Class.forName(drivers);
return DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
logger.error(InitColTable.class.getName() + ":連接數據庫的找不到驅動類");
throw new TableException(InitColTable.class.getName() + ": 連接數據庫的找不到驅動類", e);
} catch (SQLException e) {
logger.error(InitColTable.class.getName() + ":連接數據庫的sql異常");
throw new TableException(InitColTable.class.getName() + "連接數據庫的sql異常", e);
}
}
}
hibernate一般這樣連接數據庫:
public class HibernateUtils {
private static SessionFactory sf;
//靜態初始化
static{
//【1】加載配置文件
Configuration conf = new Configuration().configure();
//【2】 根據Configuration 配置信息創建 SessionFactory
sf = conf.buildSessionFactory();
//如果這裏使用了hook虛擬機,需要關閉hook虛擬機
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println("虛擬機關閉!釋放資源");
sf.close();
}
}));
}
/**
* 採用openSession創建一個與數據庫的連接會話,但這種方式需要手動關閉與數據庫的session連接(會話),
* 如果不關閉,則當前session佔用數據庫的資源無法釋放,最後導致系統崩潰。
*
**/
public static org.hibernate.Session openSession(){
//【3】 獲得session
Session session = sf.openSession();
return session;
}
/**
* 這種方式連接數據庫,當提交事務時,會自動關閉當前會話;
* 同時,創建session連接時,autoCloseSessionEnabled和flushBeforeCompletionEnabled都爲true,
* 並且session會同sessionFactory組成一個map以sessionFactory爲主鍵綁定到當前線程。
* 採用getCurrentSession()需要在Hibernate.cfg.xml配置文件中加入如下配置:
如果是本地事物,及JDBC一個數據庫:
<propety name=”Hibernate.current_session_context_class”>thread</propety>
如果是全局事物,及jta事物、多個數據庫資源或事物資源:
<propety name=”Hibernate.current_session_context_class”>jta</propety>
使用spring的getHiberanteTemplate 就不需要考慮事務管理和session關閉的問題:
*
**/
public static org.hibernate.Session getCurrentSession(){
//【3】 獲得session
Session session = sf.getCurrentSession();
return session;
}
}
mybatis的配置文件:
public class DBTools {
public static SqlSessionFactory sessionFactory;
static{
try {
//使用MyBatis提供的Resources類加載mybatis的配置文件
Reader reader = Resources.getResourceAsReader("mybatis.cfg.xml");
//構建sqlSession的工廠
sessionFactory = new SqlSessionFactoryBuilder().build(reader);
} catch (Exception e) {
e.printStackTrace();
}
}
//創建能執行映射文件中sql的sqlSession
public static SqlSession getSession(){
return sessionFactory.openSession();
}
}
hibernate、mybatis、jdbc創建於數據庫的連接方式雖然不同,但最紅都是爲了將順時態的數據寫入到數據庫中的,但這裏主要說的是hibernate。但是hibernate已經封裝了這些屬性,我們可以在configuration在配置驅動類、用戶名、用戶密碼等。再通過sessionFactory創建session會話,也就是加載Connection的物理連接,創建sql的事務,然後執行一系列的事務操作,如果事務全部成功即可成功,但反有一個失敗都會失敗。jdbc是最基礎的操作,但是,萬丈高樓平地起,只有基礎打牢,才能走的更遠。因爲hibernate封裝了這些基礎,我們操作數據庫不用考慮底層如何實現的,因而,從某種程度上來說,hibernate還是比較重的。
- hibernate爲什麼會重?
比如我們執行插入語句,可以使用save、saveOrUpdate,merge等方法。需要將實體bean通過反射轉化爲mysql的識別的SQL語句,同時,查詢雖然用到了反射,但是最後轉化出來的還是object的根對象,這時需要將根對象轉化爲當前對象,返回給客戶端。雖然很笨重,但是文件配置好了,可以大大地提高開發效率。畢竟現在的服務器的性能都比較好,公司追求的是高效率的開發,而往往不那麼看重性能,除非用戶提出性能的問題。
- 說說merge和saveOrUpdate
merge方法與saveOrUpdate從功能上類似,但他們仍有區別。現在有這樣一種情況:我們先通過session的get方法得到一個對象u,然後關掉session,再打開一個session並執行saveOrUpdate(u)。此時我們可以看到拋出異常:Exception in thread "main" org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session,即在session緩存中不允許有兩個id相同的對象。不過若使用merge方法則不會異常,其實從merge的中文意思(合併)我們就可以理解了。我們重點說說merge。merge方法產生的效果和saveOrUpdate方法相似。這是hibernate的原碼:
void saveOrUpdate(Object var1);
void saveOrUpdate(String var1, Object var2);
public Object merge(Object object);
public Object merge(String var1, Object var2);
前者不用返回任何數據,後者返回的是持久化的對象。如果根據hibernate的三種狀態,比如順時態、持久態、遊離態來說明這個問題,會比較難以理解,現在,根據參數有無id或id是否已經存在來理解merge,而且從執行他們兩個方法而產生的sql語句來看是一樣的。
-
參數實例對象沒有提供id或提供的id在數據庫找不到對應的行數據,這時merger將執行插入操作嗎,產的SQL語句如下:
Hibernate: select max(uid) from user Hibernate: insert into hibernate1.user (name, age, uid) values (?, ?, ?)
一般情況下,我們新new一個對象,或者從前端向後端傳輸javabean序列化的對象時,都不會存在當前對象的id,如果使用merge的話,就會向數據庫中插入一條數據。
- 參數實例對象的id在數據庫中已經存在,此時又有兩種情況
(1)如果對象有改動,則執行更新操作,產生sql語句有:
Hibernate: select user0_.uid as uid0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from hibernate1.user user0_ where user0_.uid=?
Hibernate: update hibernate1.user set name=?, age=? where uid=?
(2)如果對象未改動,則執行查詢操作,產生的語句有:
Hibernate: select user0_.uid as uid0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from hibernate1.user user0_ where user0_.uid=?
以上三種是什麼情況呢?如果我們保存用戶時,數據庫中肯定不存在即將添加的用戶,也就是說,我們的保存用戶就是向數據庫中添加用戶。但是,其也會跟着某些屬性, 比如說用戶需要頭像,這是多對一的關係,一個用戶可能多個對象,然而,頭像的關聯的id不是放在用戶表中的,而是放在用戶擴張表中的,這便用到了切分表的概念。題外話,我們有時會用到快照表,比如商品快照等,也許,我們購買商品時,商品是一個價格,但隨後商品的價格變了,我們需要退商品時,就不應該用到商品改變後的價格了,而是商品改變前的價格。擴展表存放用戶額外的信息,也就是用戶非必須的信息,比如說暱稱,性別,真實姓名,頭像等。 因而,頭像是圖片類型,使用hibernate的註解方式,創建用戶表、圖片表、用戶擴展表。如下所示(部分重要信息已省略)
//用戶頭像
@Entity
@Table(name = "core_user")
public class User extends BaseTenantConfObj {
/**
* 擴展表
* */
@OneToOne(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private UserExt userExt;
}
//用戶擴展表的頭像屬性
@Entity
@Table(name = "core_user_ext")
public class UserExt implements Serializable {
/**
* 頭像
*/
@ManyToOne
@JoinColumn(name = "head_logo")
private Picture headLogo;
}
//圖片表
@Entity
@Table(name = "core_picture")
public class Picture extends BaseTenantObj {
private static Logger logger = LoggerFactory.getLogger(Picture.class);
。。。。。。
//圖片存放在第三方的相對url。
@Column(name = "remote_relative_url", length = 300)
private String remoteRelativeUrl;
// 圖片大小
@Column(length = 8)
private Integer size;
/**
* 圖片所屬類型
* user_logo:用戶頭像
*/
@Column(name = "host_type", length = 58)
private String hostType;
//照片描述
@Column(name = "description", length = 255)
private String description;
}
前端代碼是:
//這裏使用到了vue.js的代碼,v-model數據的雙向綁定,前端的HTML代碼
<tr>
<td class="v-n-top">頭像:</td>
<td>
<div class="clearfix">
<input type="hidden" name="headLogo.id" v-model="pageData.userInfo.logo.id"/>
<img class="img-user-head fl" :src="(pageData.userInfo&&pageData.userInfo.logo&&pageData.userInfo.logo.path) ? (constant.imgPre + pageData.userInfo.logo.path) : 'img/user-head-default.png'">
<div class="img-btn-group">
<button cflag="upImg" type="button" class="btn btn-sm btn-up">點擊上傳</button>
<button cflag="delImg" type="button" class="btn btn-white btn-sm btn-del">刪除</button>
</div>
</div>
<p class="img-standard">推薦尺寸800*800;支持.jpg, .jpeg, .bmp, .png類型文件,1M以內</p>
</td>
</tr>
//這裏用到了js代碼,這裏用到了js的屬性方法
upImg: function(me) {
Utils.asyncImg({
fn: function(data) {
vm.pageData.userInfo.logo = {
path: data.remoteRelativeUrl,
id: data.id
};
}
});
},
上傳頭像是異步提交,如果用戶上傳了頭像,我們在提交用戶信息時,通過“headLogo.id”可以獲取當前頭像的持久化的圖片對象,hibernate首先會根據屬性headLogo找到圖片表,根據當前頭像的id找到圖片表中對應的行數據,爲什麼可以根據id來獲取行數據?
-圖片表的表結構信息
從這張圖片可以看出,圖片採用默認的存儲引擎,也就是InnoDB存儲引擎,而不是myiSam的存儲引擎。我們都知道這兩種存儲引擎的區別,如果不知道的話,可以參這篇文章MySQL中MyISAM和InnoDB的索引方式以及區別與選擇。innodb採用BTREE樹的數據結構方式存儲,它有0到1直接前繼和0到n個直接後繼,這是什麼意思呢?一棵樹當前葉子節點的直接父節點只有一個,但其兒子節點可以一個都沒有,也可以有1個、2個、3個......,如果mysql採用的多對一的方式存儲的話,你就會發現某條外鍵下有許多行數據,比如如下的這張表
這張表記錄的是項目的完成情況,一般有預約階段,合同已籤,合同完成等等。你會發現project_id=163的行數據不止一條,我們通過查詢語句:SELECT zpp.* from zq_project_process zpp WHERE zpp.is_deleted = 0 AND zpp.project_id=163
,查找速度非常快。爲什麼這麼快呢,因爲我剛開始說的innodb採用的BTREE樹結構存儲,其數據是放在當前索引下,什麼意思?innodb的存儲引擎是以索引作爲當前節點值,比如說銀行卡表的有個主鍵索引,備註,如果我們沒有創建任何索引,如果採用的innodb的數據引擎,其內部會創建一個默認的行索引,這就像我們在創建javabean對象時,沒有創建構造器,其內部會自動創建一個構造器的道理是一樣的。其數據是怎麼存儲的呢,如下圖所示:
- mysql銀行卡數據
- 其內部存儲數據
其所對應的行數據是放在當前索引下的,因而,我們取數據不是取表中中的數據,而是取當前主鍵索引下的數據。項目進程表如同銀行卡的主鍵索引,只不過其有三個索引,分別是主鍵索引和兩個外鍵索引,如圖所示的索引:
索引名是hibernate自動生成的一個名字,索引是項目id、類型兩個索引。因爲我們不是從表中取數據,而是從當前索引的節點下取數據,所以速度當然快了。索引有主鍵索引、外鍵索引、聯合索引等,但一般情況下,主鍵索引和外鍵索引使用頻率比較高。同時,innodb存儲引擎的支持事務操作,這是非常重要,我們操作數據庫,一般都是設計事務的操作,這也mysql默認的存儲引擎是innodb。
我們通過主鍵獲取圖片的行數據,就像通過主鍵獲取銀行卡的行數據。這也是上面所說的,根據是否有id來確定是插入還是更新數據。通過圖片主鍵id獲取該行數據後,hibernate會在堆中創建一個picture對象。用戶擴展表的headLogo屬性指向這個圖片對象的首地址,從而創建一個持久化的圖片對象。前臺異步提交頭像時,如果是編輯頭像,hibernate會覺擦到當前對象的屬性發生了改變,於是,在提交事務時將修改後的遊離態的類保存到數據庫中。如果我們保存或修改用戶時,我們保存的就是持久化的對象,其內部會自動存儲持久化頭像的id。這是hibernate底層所做的,我們不需要關心。
- 再舉一個hibernate事務提交的例子:
我們在支付當中搞得提現事務時,調用第三方支付的SDK時,第三方一般會用我們到訂單號,比如我們調用連連支付這個第三方支付的SDK的payRequestBean的實體類:
/**
* Created By zby on 11:00 2018/12/11
* 發送到連連支付的body內容
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PaymentRequestBean extends BaseRequestBean {
/**
* 版本號
*/
@NonNull
private String api_version;
/**
* 銀行賬戶
*/
@NonNull
private String card_no;
/**
* 對私
*/
@NonNull
private String flag_card;
/**
* 回調接口
*/
@NonNull
private String notify_url;
/**
* 商戶訂單號
*/
@NonNull
private String no_order;
/**
* 商戶訂單時間,時間格式爲 YYYYMMddHHmmss
*/
@NonNull
private String dt_order;
/**
* 交易金額
*/
@NonNull
public String money_order;
/**
* 收款方姓名 即賬戶名
*/
@NonNull
private String acct_name;
/**
* 收款銀行姓名
*/
private String bank_name;
/**
* 訂單描述 ,代幣類型 + 支付
*/
@NonNull
private String info_order;
/**
* 收款備註
*/
private String memo;
/**
* 支行名稱
*/
private String brabank_name;
}
商戶訂單號是必傳的,且這個訂單號是我們這邊提供的,這就有一個問題了,怎麼避免訂單號不重複呢?我們可以在提現記錄表事先存儲一個訂單號,訂單號的規則如下:"WD" +系統時間+ 當前提現記錄的id,這個id怎麼拿到呢?既然底層使用的是merge方法,我們事先不創建訂單號,先保存這個記錄,其返回的是已經創建好的持久化的對象,該持久化的對象肯定有提現主鍵的id。我們拿到該持久化對象的主鍵id,便可以封裝訂單號,再次保存這個持久化的對象,其內部會執行類似以下的操作:
Hibernate: select user0_.uid as uid0_0_, user0_.name as name0_0_, user0_.age as age0_0_
from hibernate1.user user0_ where user0_.uid=?
Hibernate: update hibernate1.user set name=?, age=? where uid=?
代碼如下:
withdraw.setWithdrawStatus(WITHDRAW_STATUS_WAIT_PAY);
withdraw.setApplyTime(currentTime);
withdraw.setExchangeHasThisMember(hasThisMember ? YES : NO);
withdraw = withdrawDao.save(withdraw);
withdraw.setOrderNo("WD" + DateUtil.ISO_DATETIME_FORMAT_NONE.format(currentTime) + withdraw.getId());
withdrawDao.save(withdraw);
不管哪種情況,merge的返回值都是一個持久化的實例對象,但對於參數而言不會改變它的狀態。