hibernate和jdbc的淵源

介紹jdbc

咱們學習Java數據庫操做時,通常會設計到jdbc的操做,這是一位程序員最基本的素養。jdbc以其優美的代碼和高性能,將瞬時態的javabean對象轉化爲持久態的SQL數據。可是,每次SQL操做都須要創建和關閉鏈接,這勢必會消耗大量的資源開銷;若是咱們自行建立鏈接池,假如每一個項目都這樣作,勢必搞死的了。同時,咱們將SQL語句預編譯在PreparedStatement中,這個類可使用佔位符,避免SQL注入,固然,後面說到的hibernate的佔位符的原理也是這樣,同時,mybatis的#{}佔位符原理也是如此。預編譯的語句是原生的SQL語句,好比更新語句:php

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操做html

於是,咱們在確定它的優勢時,也不該該規避他的缺點。隨着工業化步伐的推動,每一個數據庫每每涉及到不少表,每張表有可能會關聯到其餘表,若是咱們仍是按照jdbc的方式操做,這無疑是增長了開發效率。因此,有人厭倦這種複雜繁瑣、效率低下的操做,因而,寫出了著名的hibernate框架,封裝了底層的jdbc操做,如下是jdbc的優缺點:前端

jdbc的有點和缺點

由上圖能夠看見,jdbc不適合公司的開發,公司畢竟以最少的開發成原本創造更多的利益。這就出現了痛點,商機伴隨着痛點的出現。於是,應世而生了hibernate這個框架。即使沒有hibernate的框架,也會有其餘框架生成。hibernate的底層封裝了jdbc,好比說jdbc爲了防止sql注入,通常會有佔位符,hibernate也會有響應的佔位符。hibernate是orm(object relational mapping)的一種,即對象關係映射。vue


對象關係映射

通俗地來講,對象在pojo中能夠指Javabean,關係能夠指MySQL的關係型數據庫的表字段與javabean對象屬性的關係。映射能夠用咱們高中所學的函數映射,即Javabean瞬時態的對象映射到數據庫的持久態的數據對象。咱們都知道javabean在內存中的數據是瞬時狀態或遊離狀態,就像是宇宙星空中的一顆顆行星,除非行星被其餘行星所吸引,纔有可能不成爲遊離的行星。瞬時狀態(遊離狀態)的javabean對象的生命週期隨着進程的關閉或者方法的結束而結束。若是當前javabean對象與gcRoots沒有直接或間接的關係,其有可能會被gc回收。咱們就沒辦法長久地存儲數據,這是一個很是頭疼的問題。假如咱們使用文件來存儲數據,可是文件操做起來很是麻煩,並且數據格式不是很整潔,不利於後期的維護等。於是,橫空出世了數據庫。咱們可使用數據庫存儲數據,數據庫中的數據纔是持久數據(數據庫的持久性),除非人爲的刪除。這裏有個問題——怎麼將瞬時狀態(遊離狀態)的數據轉化爲持久狀態的數據,確定須要一個鏈接Javabean和數據庫的橋樑,因而乎就有了上面的jdbc。java

單獨來講mysql,mysql是一個遠程服務器。咱們在向mysql傳輸數據時,並非對象的方式傳輸,而是以字節碼的方式傳輸數據。爲了保證數據準確的傳輸,咱們通常會序列化當前對象,用序列號標誌這個惟一的對象。若是,咱們不想存儲某個屬性,它是有數據庫中的數據拼接而成的,咱們大可不用序列化這個屬性,可使用Transient來修飾。好比下面的獲取圖片的路徑,其實就是服務器圖片的文件夾地址和圖片的名稱拼接而成的。固然,你想存儲這個屬性,也能夠存儲這個屬性。咱們有時候圖片的路由的字節很長,這樣會佔用MySQL的內存。於是,學會取捨,何嘗不是一個明智之舉。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並不懂技術,他們只是照本宣科的提問。若是你照本宣科的回答,它們覺着你很厲害。可是,若是是一個懂技術的人提問你,若是你只是臨時背了它們的區別,而沒有相應的工做經驗,他們會問的讓你手足無措。web


hibernate講解

由於咱們公司使用的是hibernate,我在這裏簡單地介紹下hibernate。但相對於jdbc來講,hibernate框架仍是比較重的。爲何說他重,由於它集成了太多的東西,看以下的hibernate架構圖:面試

hibernate_architecture.jpg

你會發現最上層使咱們的java應用程序的開始,好比web的Tomcat服務器的啓動,好比main方法的啓動等。緊接着就是須要(needing)持久化的對象,這裏爲何說是須要,而不是持久化的對象。只有保存到文件、數據庫中的數據纔是持久化的。想經過hibernate,咱們能夠絕不費力的將瞬時狀態的數據轉化爲持久狀態的數據,下面即是hibernate的內部操做數據。其通常是這樣的流程:spring

  • 我我的畫的

hibernate內部流程

1055646-20170616114130884-2134062939.png

若是你是用過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爲何重(zhong)?

好比咱們執行插入語句,可使用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的存儲引擎。


innodb存儲引擎

咱們都知道這兩種存儲引擎的區別,若是不知道的話,能夠參這篇文章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的返回值都是一個持久化的實例對象,但對於參數而言不會改變它的狀態。

相關文章
相關標籤/搜索