hibernate和jdbc的淵源

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的有點和缺點

由上圖可以看見,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架構圖:

hibernate_architecture.jpg

你會發現最上層使我們的java應用程序的開始,比如web的Tomcat服務器的啓動,比如main方法的啓動等。緊接着就是需要(needing)持久化的對象,這裏爲什麼說是需要,而不是持久化的對象。只有保存到文件、數據庫中的數據纔是持久化的想通過hibernate,我們可以毫不費力的將瞬時狀態的數據轉化爲持久狀態的數據,下面便是hibernate的內部操作數據。其一般是這樣的流程:

  • 我個人畫的

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爲什麼會重?

比如我們執行插入語句,可以使用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的返回值都是一個持久化的實例對象,但對於參數而言不會改變它的狀態。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章