從JDBC到ORM的事務實現

一、JDBC

早期SUN公司想編寫一套可以連接天下所有資料庫的API,但是當他們剛剛開始時就發現這是不可完成的任務,因為各個廠商的資料庫伺服器差異太大了。後來SUN開始與資料庫廠商們討論,最終得出的結論是,由SUN提供一套訪問資料庫的規範(就是一組介面),並提供連接資料庫的協議標準,然後各個資料庫廠商會遵循SUN的規範提供一套訪問自己公司的資料庫伺服器的API出現。SUN提供的規範命名為JDBC,而各個廠商提供的,遵循了JDBC規範的,可以訪問自己資料庫的API被稱之為驅動。

資料庫連接池:

C3P0

DBCP– Apache CommonPool

Druid

Hikari

二、ORM

Hibernate

Hibernate 是一個開源的對象關係映射框架,它對
JDBC 進行了非常輕量級的對象封裝,它將 POJO 與
資料庫表建立映射關係,是一個全自動的 orm 框架
,hibernate 可以自動生成 SQL 語句,自動執行,
使得 Java 程式設計師可以使用面向對象的思維來操縱數
據庫。
Hibernate 里需要定義實體類和 hbm 映射關係文件
(IDE 一般有工具生成)。
Hibernate 里可以使用 HQL、Criteria、Native SQL
三種方式操作資料庫。
也可以作為 JPA 適配實現,使用 JPA 介面操作。

Mybatis

MyBatis 是一款優秀的持久層框架,它支援訂製化 SQL、存儲過程以及高級映射。MyBatis避免了幾乎所有的JDBC 程式碼和手動設置參數以及獲取結果集。Mybatis 可以使用簡單的XML或註解來配置和映射原生資訊,將介面和 Java的POJOs(Plain Old Java Objects,普通的Java對象)映射成資料庫中的記錄

Mybatis與Hibernate的區別?

Hibernate是全自動,Mybatis是半自動。

Mybatis

  • 優點:原生SQL(XML語法),直觀,容易優化
  • 缺點:繁瑣,可以用Mybatis-generator、Mybatis-Plus之類的插件彌補

Hibernate

  • 優點:簡單場景不用寫SQL(HQL、Cretiria、SQL)
  • 缺點:不好優化sql,對DBA不友好

Spring管理事務

我們先看看JDBC如何操作事務?

   public void updatePrice() throws SQLException {
        try {
            Connection conn = getConnection();
            //關閉自動提交
            conn.setAutoCommit(false);
            String sql = "update goods set price =? where id=? ";
            PreparedStatement ptmt = conn.prepareStatement(sql); 
            ptmt.setDouble(1, 3500);
            ptmt.setString(2, "1");
            //執行
            ptmt.execute();
            //提交事務
            conn.commit();
        } catch (Exception e) {
            //回滾事務
           conn.rollback();
           e.printStackTrace();
        }
    }

再看看Spring是如何無侵入的進行事務管理的?

實現原理:事務管理器+AOP

源碼分析Spring事務實現過程

示例程式碼:
我在goodsService.updatePrice方法上加了事務註解。

 @RequestMapping("/updateprice")
    public String updateprice(Double price,Integer age){
        goodsService.updatePrice(1,price);
        int i=10/0;
        userService.updateAge(1,age);
        return "sucess";
    }
  1. 請求進入controller,調用goodsService的時候,調用的實際上是goodsService的代理對象

  2. 到代理類的方法中org.springframework.aop.framework.ReflectiveMethodInvocation#proceed

  3. 進入事務攔截器的方法

    裡面有個TransactionInterceptor

  4. TransactionInterceptor方法裡面進行了事務管理

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable {

		// If the transaction attribute is null, the method is non-transactional.
		TransactionAttributeSource tas = getTransactionAttributeSource();
		final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
		final PlatformTransactionManager tm = determineTransactionManager(txAttr);
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

		if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
			// Standard transaction demarcation with getTransaction and commit/rollback calls.
			TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
			Object retVal = null;
			try {
				// This is an around advice: Invoke the next interceptor in the chain.
				// This will normally result in a target object being invoked.
				//執行目標方法
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				//回滾
				exception
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
				cleanupTransactionInfo(txInfo);
			}
			//提交事務
			commitTransactionAfterReturning(txInfo);
			return retVal;
		}
		
		...

上述是簡單場景的事務處理,如果是多個service方法,並且都加了@Transactional註解,那事務怎麼算呢?那就需要學習Spring里的事務傳播了。

public Class ServiceA{
    
    @Transactional
    void methodA(){
        //....    
    }
}


public Class ServiceB{
    
    @Transactional
    void methodB(){
        //....    
    }
}

七種事務的傳播行為:

  • PROPAGATION_REQUIRED (默認) 表示當前方法必須在一個具有事務的上下文中運行,如有客戶端有事務在進行,那麼被調用端將在該事務中運行,否則的話重新開啟一個事務。

  • PROPAGATION_SUPPORTS 表示當前方法不必須在一個具有事務的上下文中運行,如:ServiceA.methodA()調用ServiceB.methodB(),如果methodA方法上有事務,那麼methodB加入他的事務,如果methodA上沒有事務,那麼methodB也不開事務

  • PROPAGATION_MANDATORY 必須被開啟事務的方法調用,否則報錯

  • PROPAGATION_REQUIRES_NEW 強制自己開啟一個新的事務,如果一個事務已經存在,那麼將這個事務掛起.如ServiceA.methodA()調用ServiceB.methodB(),methodB()上的傳播級別是PROPAGATION_REQUIRES_NEW的話,那麼如果methodA報錯,不影響methodB的事務,如果methodB報錯,那麼methodA是可以選擇是回滾或者提交的,就看你是否將methodB報的錯誤拋出還是try catch了.

  • PROPAGATION_NOT_SUPPORTED 總是非事務的執行,並且掛起任何事務.就是如果methodA方法執行到methodB這裡了,methodA的事務就被掛起,然後methodB非事務的執行,然後等methodB方法運行結束,methodA的事務再繼續.這個的好處就是methodB報錯了不會讓methodA回滾.

  • PROPAGATION_NEVER 總是非事務地執行,如果存在一個活動事務,則拋出異常

  • PROPAGATION_NESTED 表示如果當前方法正有一個事務在運行中,則該方法應該運行在一個嵌套事務中 ,被嵌
    套的事務可以獨立於被封裝的事務中進行提交或者回滾。如果封裝事務存在,並且外層事務拋出異常回滾,那麼內層事務必須回滾,反之,內層事務並不影響外層事務。如果封裝事務不存在,則同propagation. required的一樣

事務失效的幾個原因:

  1. spring的事務註解@Transactional只能放在public修飾的方法上才起作用,如果放在其他非public(private,protected)方法上,雖然不報錯,但是事務不起作用
  2. 如果採用spring+springmvc,則context:component-scan重複掃描問題可能會引起事務失敗。如果spring和mvc的配置文件中都掃描了service層,那麼事務就會失效。
    原因:因為按照spring配置文件的載入順序來講,先載入springmvc配置文件,再載入spring配置文件,我們的事物一般都在srping配置文件中進行配置,如果此時在載入srpingMVC配置文件的時候,把servlce也給註冊了,但是此時事物還沒載入,也就導致後面的事物無法成功注入到service中。所以把對service的掃描放在spring配置文件中或是其他配置文件中。
  3. 如使用mysql且引擎是MyISAM,則事務會不起作用,原因是MyISAM不支援事務,可以改成InnoDB引擎
  4. @Transactional註解開啟配置,必須放到listener里載入,如果放到DispatcherServlet的配置里,事務也是不起作用的。
  5. Spring團隊建議在具體的類(或類的方法)上使用 @Transactional 註解,而不要使用在類所要實現的任何介面上。在介面上使用 @Transactional註解,只能當你設置了基於介面的代理時它才生效。因為註解是 不能繼承 的,這就意味著如果正在使用基於類的代理時,那麼事務的設置將不能被基於類的代理所識別,而且對象也將不會被事務代理所包裝。
  6. 在業務程式碼中如果拋出RuntimeException異常,事務回滾;但是拋出Exception,事務不回滾;默認對RuntimeException回滾
  7. 如果在加有事務的方法內,使用了try…catch..語句塊對異常進行了捕獲,而catch語句塊沒有throw new RuntimeExecption異常,事務也不會回滾
  8. 在類A裡面有方法a 和方法b, 然後方法b上面用 @Transactional加了方法級別的事務,在方法a裡面 調用了方法b,方法b裡面的事務不會生效。原因是在同一個類之中,方法互相調用,切面無效,而不僅僅是事務。這裡事務之所以無效,是因為spring的事務是通過aop實現的。

程式碼示例:
可以看出這個this,並不是代理對象,事務也就不能生效了。