溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Spring不同數(shù)據(jù)庫sql異常的轉(zhuǎn)換方法

發(fā)布時間:2021-09-07 13:43:10 來源:億速云 閱讀:180 作者:chen 欄目:開發(fā)技術(shù)

這篇文章主要介紹“Spring不同數(shù)據(jù)庫sql異常的轉(zhuǎn)換方法”,在日常操作中,相信很多人在Spring不同數(shù)據(jù)庫sql異常的轉(zhuǎn)換方法問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Spring不同數(shù)據(jù)庫sql異常的轉(zhuǎn)換方法”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

目錄
  • 前言

  • 代碼實現(xiàn)

  • 處理流程圖

  • 用到了哪些設(shè)計模式?

    • 組合模式

    • 單例模式

    • 策略模式

  • 總結(jié):

    前言

    使用Spring-Jdbc的情況下,在有些場景中,我們需要根據(jù)數(shù)據(jù)庫報的異常類型的不同,來編寫我們的業(yè)務(wù)代碼。比如說,我們有這樣一段邏輯,如果我們新插入的記錄,存在唯一約束沖突,就會返回給客戶端描述:記錄已存在,請勿重復(fù)操作

    代碼一般是這么寫的:

    @Resource
    private JdbcTemplate jdbcTemplate;
    public String testAdd(){
        try {
            jdbcTemplate.execute("INSERT INTO user_info (user_id, user_name, email, nick_name, status, address) VALUES (80002, '張三豐', 'xxx@126.com', '張真人', 1, '武當(dāng)山');");
            return "OK";
        }catch (DuplicateKeyException e){
            return "記錄已存在,請勿重復(fù)操作";
        }
    }

    測試一下:

    Spring不同數(shù)據(jù)庫sql異常的轉(zhuǎn)換方法

    如上圖提示,并且無論什么更換什么數(shù)據(jù)庫(Spring-Jdbc支持的),代碼都不用改動

    那么Spring-Jdbc是在使用不同數(shù)據(jù)庫時,Spring如何幫我們實現(xiàn)對異常的抽象的呢?

    代碼實現(xiàn)

    我們來正向看下代碼:

    首先入口JdbcTemplate.execute方法:

    public void execute(final String sql) throws DataAccessException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Executing SQL statement [" + sql + "]");
        }
        ...
        //實際執(zhí)行入口,調(diào)用內(nèi)部方法
        this.execute(new ExecuteStatementCallback(), true);
    }

    內(nèi)部方法execute

    @Nullable
    private <T> T execute(StatementCallback<T> action, boolean closeResources) throws DataAccessException {
        Assert.notNull(action, "Callback object must not be null");
        Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
        Statement stmt = null;
    
        Object var12;
        try {
    	...
        } catch (SQLException var10) {
            ....
    	//SQL出現(xiàn)異常后,所有的異常在這里進行異常轉(zhuǎn)換
            throw this.translateException("StatementCallback", sql, var10);
        } finally {
            if (closeResources) {
                JdbcUtils.closeStatement(stmt);
                DataSourceUtils.releaseConnection(con, this.getDataSource());
            }
    
        }
    
        return var12;
    }

    異常轉(zhuǎn)換方法translateException

    protected DataAccessException translateException(String task, @Nullable String sql, SQLException ex) {
    	//獲取異常轉(zhuǎn)換器,然后根據(jù)數(shù)據(jù)庫返回碼相關(guān)信息執(zhí)行轉(zhuǎn)換操作
    	//轉(zhuǎn)換不成功,也有兜底異常UncategorizedSQLException
        DataAccessException dae = this.getExceptionTranslator().translate(task, sql, ex);
        return (DataAccessException)(dae != null ? dae : new UncategorizedSQLException(task, sql, ex));
    }

    獲取轉(zhuǎn)換器方法getExceptionTranslator

    public SQLExceptionTranslator getExceptionTranslator() {
        //獲取轉(zhuǎn)換器屬性,如果為空,則生成一個
        SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator;
        if (exceptionTranslator != null) {
            return exceptionTranslator;
        } else {
            synchronized(this) {
                SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator;
    		if (exceptionTranslator == null) {
                    DataSource dataSource = this.getDataSource();
                    //shouldIgnoreXml是一個標(biāo)記,就是不通過xml加載bean,默認false
    		if (shouldIgnoreXml) {
                        exceptionTranslator = new SQLExceptionSubclassTranslator();
                    } else if (dataSource != null) {
    		//如果DataSource不為空,則生成轉(zhuǎn)換器SQLErrorCodeSQLExceptionTranslator,一般情況下首先獲取到該轉(zhuǎn)換器
                        exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
                    } else {
    		// 其他情況,生成SQLStateSQLExceptionTranslator轉(zhuǎn)換器
                        exceptionTranslator = new SQLStateSQLExceptionTranslator();
                    }
    
                    this.exceptionTranslator = (SQLExceptionTranslator)exceptionTranslator;
                }
                return (SQLExceptionTranslator)exceptionTranslator;
            }
        }
    }

    轉(zhuǎn)換方法:

    因為默認的轉(zhuǎn)換器是SQLErrorCodeSQLExceptionTranslator,所以這里調(diào)用SQLErrorCodeSQLExceptionTranslator的doTranslate方法

    Spring不同數(shù)據(jù)庫sql異常的轉(zhuǎn)換方法

    類圖調(diào)用關(guān)系如上,實際先調(diào)用的是AbstractFallbackSQLExceptionTranslator.translate的方法

    @Nullable
    public DataAccessException translate(String task, @Nullable String sql, SQLException ex) {
        Assert.notNull(ex, "Cannot translate a null SQLException");
    		//這里才真正調(diào)用SQLErrorCodeSQLExceptionTranslator.doTranslate方法
        DataAccessException dae = this.doTranslate(task, sql, ex);
        if (dae != null) {
            return dae;
        } else {
    		    //如果沒有找到響應(yīng)的異常,則調(diào)用其他轉(zhuǎn)換器,輸入遞歸調(diào)用,這里后面說
            SQLExceptionTranslator fallback = this.getFallbackTranslator();
            return fallback != null ? fallback.translate(task, sql, ex) : null;
        }
    }

    實際轉(zhuǎn)換類SQLErrorCodeSQLExceptionTranslator的方法:

    //這里省略了一些無關(guān)代碼,只保留了核心代碼
    //先獲取SQLErrorCodes集合,在根據(jù)返回的SQLException中獲取的ErrorCode進行匹配,根據(jù)匹配結(jié)果進行返回響應(yīng)的異常
    protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
    	....
    	SQLErrorCodes sqlErrorCodes = this.getSqlErrorCodes();
        
    	String errorCode = Integer.toString(ex.getErrorCode());
    	...
    	//這里用1062唯一性約束沖突,所以走到這里的邏輯,從而返回DuplicateKeyException
    	if (Arrays.binarySearch(sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) {
    		this.logTranslation(task, sql, sqlEx, false);
    		return new DuplicateKeyException(this.buildMessage(task, sql, sqlEx), sqlEx);
    	}
        ...
        return null;
    }

    上面的SQLErrorCodes是一個錯誤碼集合,但是不是全部數(shù)據(jù)庫的所有錯誤碼集合,而是只取了相應(yīng)數(shù)據(jù)庫的錯誤碼集合,怎么保證獲取的是當(dāng)前使用的數(shù)據(jù)庫的錯誤碼,而不是其他數(shù)據(jù)庫的錯誤碼呢?當(dāng)然Spring為我們實現(xiàn)了,在SQLErrorCodeSQLExceptionTranslator中:

    public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator {
    
    private SingletonSupplier<SQLErrorCodes> sqlErrorCodes;
    //默認構(gòu)造方法,設(shè)置了如果轉(zhuǎn)換失敗,下一個轉(zhuǎn)換器是SQLExceptionSubclassTranslator
        public SQLErrorCodeSQLExceptionTranslator() {
            this.setFallbackTranslator(new SQLExceptionSubclassTranslator());
    }
    //前面生成轉(zhuǎn)換器的時候,exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
    //使用的是本構(gòu)造方法,傳入了DataSource,其中有數(shù)據(jù)庫廠商信息,本文中是MYSQL
    public SQLErrorCodeSQLExceptionTranslator(DataSource dataSource) {
    	this();
    	this.setDataSource(dataSource);
    }
    
    //從錯誤碼工廠SQLErrorCodesFactory里,獲取和數(shù)據(jù)源對應(yīng)的廠商的所有錯誤碼
    public void setDataSource(DataSource dataSource) {
    	this.sqlErrorCodes = SingletonSupplier.of(() -> {
    		return SQLErrorCodesFactory.getInstance().resolveErrorCodes(dataSource);
    	});
    	this.sqlErrorCodes.get();
    }
    }

    錯誤碼工廠SQLErrorCodesFactory的resolveErrorCodes方法:

    //既然是工廠,里面肯定有各種數(shù)據(jù)庫的錯誤碼,本文中使用的是MYSQL,我們看一下實現(xiàn)邏輯
    @Nullable
    public SQLErrorCodes resolveErrorCodes(DataSource dataSource) {
        Assert.notNull(dataSource, "DataSource must not be null");
        if (logger.isDebugEnabled()) {
            logger.debug("Looking up default SQLErrorCodes for DataSource [" + this.identify(dataSource) + "]");
        }
        //從緩存中拿MYSQL對應(yīng)的SQLErrorCodes
        SQLErrorCodes sec = (SQLErrorCodes)this.dataSourceCache.get(dataSource);
        if (sec == null) {
            synchronized(this.dataSourceCache) {
                sec = (SQLErrorCodes)this.dataSourceCache.get(dataSource);
                if (sec == null) {
                    try {
                        String name = (String)JdbcUtils.extractDatabaseMetaData(dataSource, DatabaseMetaData::getDatabaseProductName);
                        if (StringUtils.hasLength(name)) {
                            SQLErrorCodes var10000 = this.registerDatabase(dataSource, name);
                            return var10000;
                        }
                    } catch (MetaDataAccessException var6) {
                        logger.warn("Error while extracting database name", var6);
                    }
    
                    return null;
                }
            }
        }
    
        if (logger.isDebugEnabled()) {
            logger.debug("SQLErrorCodes found in cache for DataSource [" + this.identify(dataSource) + "]");
        }
    
        return sec;
    }

    緩存dataSourceCache如何生成的?

    public SQLErrorCodes registerDatabase(DataSource dataSource, String databaseName) {
       //根據(jù)數(shù)據(jù)庫類型名稱(這里是MySQL),獲取錯誤碼列表
        SQLErrorCodes sec = this.getErrorCodes(databaseName);
        if (logger.isDebugEnabled()) {
            logger.debug("Caching SQL error codes for DataSource [" + this.identify(dataSource) + "]: database product name is '" + databaseName + "'");
        }
    
        this.dataSourceCache.put(dataSource, sec);
        return sec;
    }
    
    public SQLErrorCodes getErrorCodes(String databaseName) {
            Assert.notNull(databaseName, "Database product name must not be null");
    	//從errorCodesMap根據(jù)key=MYSQL獲取SQLErrorCodes
            SQLErrorCodes sec = (SQLErrorCodes)this.errorCodesMap.get(databaseName);
            if (sec == null) {
                Iterator var3 = this.errorCodesMap.values().iterator();
    
                while(var3.hasNext()) {
                    SQLErrorCodes candidate = (SQLErrorCodes)var3.next();
                    if (PatternMatchUtils.simpleMatch(candidate.getDatabaseProductNames(), databaseName)) {
                        sec = candidate;
                        break;
                    }
                }
            }
    
            if (sec != null) {
                this.checkCustomTranslatorRegistry(databaseName, sec);
                if (logger.isDebugEnabled()) {
                    logger.debug("SQL error codes for '" + databaseName + "' found");
                }
    
                return sec;
            } else {
                if (logger.isDebugEnabled()) {
                    logger.debug("SQL error codes for '" + databaseName + "' not found");
                }
    
                return new SQLErrorCodes();
            }
        }
    		
    		
    //SQLErrorCodesFactory構(gòu)造方法中,生成的errorCodesMap,map的內(nèi)容來自org/springframework/jdbc/support/sql-error-codes.xml文件		
    protected SQLErrorCodesFactory() {
            Map errorCodes;
            try {
                DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
                lbf.setBeanClassLoader(this.getClass().getClassLoader());
                XmlBeanDefinitionReader bdr = new XmlBeanDefinitionReader(lbf);
                Resource resource = this.loadResource("org/springframework/jdbc/support/sql-error-codes.xml");
                if (resource != null && resource.exists()) {
                    bdr.loadBeanDefinitions(resource);
                } else {
                    logger.info("Default sql-error-codes.xml not found (should be included in spring-jdbc jar)");
                }
    
                resource = this.loadResource("sql-error-codes.xml");
                if (resource != null && resource.exists()) {
                    bdr.loadBeanDefinitions(resource);
                    logger.debug("Found custom sql-error-codes.xml file at the root of the classpath");
                }
    
                errorCodes = lbf.getBeansOfType(SQLErrorCodes.class, true, false);
                if (logger.isTraceEnabled()) {
                    logger.trace("SQLErrorCodes loaded: " + errorCodes.keySet());
                }
            } catch (BeansException var5) {
                logger.warn("Error loading SQL error codes from config file", var5);
                errorCodes = Collections.emptyMap();
            }
    
            this.errorCodesMap = errorCodes;
    }

    sql-error-codes.xml文件中配置了各個數(shù)據(jù)庫的主要的錯誤碼

    這里列舉了MYSQL部分,當(dāng)然還有其他部分,我們可以看到唯一性約束錯誤碼是1062,就可以翻譯成DuplicateKeyException異常了

    <bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes">
    		<property name="databaseProductNames">
    			<list>
    				<value>MySQL</value>
    				<value>MariaDB</value>
    			</list>
    		</property>
    		<property name="badSqlGrammarCodes">
    			<value>1054,1064,1146</value>
    		</property>
    		<property name="duplicateKeyCodes">
    			<value>1062</value>
    		</property>
    		<property name="dataIntegrityViolationCodes">
    			<value>630,839,840,893,1169,1215,1216,1217,1364,1451,1452,1557</value>
    		</property>
    		<property name="dataAccessResourceFailureCodes">
    			<value>1</value>
    		</property>
    		<property name="cannotAcquireLockCodes">
    			<value>1205,3572</value>
    		</property>
    		<property name="deadlockLoserCodes">
    			<value>1213</value>
    		</property>
    	</bean>

    你已經(jīng)看到,比如上面的錯誤碼值列舉了一部分,如果出現(xiàn)了一個不在其中的錯誤碼肯定是匹配不到,Spring當(dāng)然能想到這種情況了

       /**
    	 *@公-眾-號:程序員阿牛
    	 *在AbstractFallbackSQLExceptionTranslator中,看到如果查找失敗會獲取下一個后續(xù)轉(zhuǎn)換器
    	 */
        @Nullable
        public DataAccessException translate(String task, @Nullable String sql, SQLException ex) {
            Assert.notNull(ex, "Cannot translate a null SQLException");
            DataAccessException dae = this.doTranslate(task, sql, ex);
            if (dae != null) {
                return dae;
            } else {
                SQLExceptionTranslator fallback = this.getFallbackTranslator();
                return fallback != null ? fallback.translate(task, sql, ex) : null;
            }
        }

    SQLErrorCodeSQLExceptionTranslator的后置轉(zhuǎn)換器是什么?

    //構(gòu)造方法中已經(jīng)指定,SQLExceptionSubclassTranslator
    public SQLErrorCodeSQLExceptionTranslator() {
       this.setFallbackTranslator(new SQLExceptionSubclassTranslator());
    }

    SQLExceptionSubclassTranslator的轉(zhuǎn)換方法邏輯如下:

    /**
    *@公-眾-號:程序員阿牛
    *可以看出實際按照子類類型來判斷,返回相應(yīng)的錯誤類,如果匹配不到,則找到下一個處理器,這里的處理其我們可以根據(jù)構(gòu)造方法青松找到*SQLStateSQLExceptionTranslator
    */
    @Nullable
    protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
        if (ex instanceof SQLTransientException) {
            if (ex instanceof SQLTransientConnectionException) {
                return new TransientDataAccessResourceException(this.buildMessage(task, sql, ex), ex);
            }
    
            if (ex instanceof SQLTransactionRollbackException) {
                return new ConcurrencyFailureException(this.buildMessage(task, sql, ex), ex);
            }
    
            if (ex instanceof SQLTimeoutException) {
                return new QueryTimeoutException(this.buildMessage(task, sql, ex), ex);
            }
        } else if (ex instanceof SQLNonTransientException) {
            if (ex instanceof SQLNonTransientConnectionException) {
                return new DataAccessResourceFailureException(this.buildMessage(task, sql, ex), ex);
            }
    
            if (ex instanceof SQLDataException) {
                return new DataIntegrityViolationException(this.buildMessage(task, sql, ex), ex);
            }
    
            if (ex instanceof SQLIntegrityConstraintViolationException) {
                return new DataIntegrityViolationException(this.buildMessage(task, sql, ex), ex);
            }
    
            if (ex instanceof SQLInvalidAuthorizationSpecException) {
                return new PermissionDeniedDataAccessException(this.buildMessage(task, sql, ex), ex);
            }
    
            if (ex instanceof SQLSyntaxErrorException) {
                return new BadSqlGrammarException(task, sql != null ? sql : "", ex);
            }
    
            if (ex instanceof SQLFeatureNotSupportedException) {
                return new InvalidDataAccessApiUsageException(this.buildMessage(task, sql, ex), ex);
            }
        } else if (ex instanceof SQLRecoverableException) {
            return new RecoverableDataAccessException(this.buildMessage(task, sql, ex), ex);
        }
    
        return null;
    }

    SQLStateSQLExceptionTranslator的轉(zhuǎn)換方法:

    /**
    *@公-眾-號:程序員阿牛
    *可以看出根據(jù)SQLState的前兩位來判斷異常,根據(jù)匹配結(jié)果返回相應(yīng)的異常信息  
    */
    @Nullable
    protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
        String sqlState = this.getSqlState(ex);
        if (sqlState != null && sqlState.length() >= 2) {
            String classCode = sqlState.substring(0, 2);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Extracted SQL state class '" + classCode + "' from value '" + sqlState + "'");
            }
    
            if (BAD_SQL_GRAMMAR_CODES.contains(classCode)) {
                return new BadSqlGrammarException(task, sql != null ? sql : "", ex);
            }
    
            if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) {
                return new DataIntegrityViolationException(this.buildMessage(task, sql, ex), ex);
            }
    
            if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) {
                return new DataAccessResourceFailureException(this.buildMessage(task, sql, ex), ex);
            }
    
            if (TRANSIENT_DATA_ACCESS_RESOURCE_CODES.contains(classCode)) {
                return new TransientDataAccessResourceException(this.buildMessage(task, sql, ex), ex);
            }
    
            if (CONCURRENCY_FAILURE_CODES.contains(classCode)) {
                return new ConcurrencyFailureException(this.buildMessage(task, sql, ex), ex);
            }
        }
    
        return ex.getClass().getName().contains("Timeout") ? new QueryTimeoutException(this.buildMessage(task, sql, ex), ex) : null;
    }

    為什么SQLState可以得出錯誤類型?

    因為數(shù)據(jù)庫是根據(jù) X/Open 和 SQL Access Group SQL CAE 規(guī)范 (1992) 所進行的定義,SQLERROR 返回 SQLSTATE 值。SQLSTATE 值是包含五個字符的字符串 。五個字符包含數(shù)值或者大寫字母, 代表各種錯誤或者警告條件的代碼。SQLSTATE 有個層次化的模式:頭兩個字符標(biāo)識條件的通常表示錯誤條件的類別, 后三個字符表示在該通用類中的子類。成功的狀態(tài)是由 00000 標(biāo)識的。SQLSTATE 代碼在大多數(shù)地方都是定義在 SQL 標(biāo)準(zhǔn)里

    處理流程圖

    Spring不同數(shù)據(jù)庫sql異常的轉(zhuǎn)換方法

    用到了哪些設(shè)計模式?

    組合模式

    Spring不同數(shù)據(jù)庫sql異常的轉(zhuǎn)換方法

    通過上圖大家有沒有發(fā)現(xiàn)三個實現(xiàn)類之間的關(guān)系—組合關(guān)系,組合關(guān)系在父類AbstractFallbackSQLExceptionTranslator中變成了遞歸調(diào)用,這里充滿了智慧(Composite設(shè)計模式)。

    單例模式

    在SQLErrorCodesFactory(單例模式)

    策略模式

    根據(jù)數(shù)據(jù)庫的不同,獲取不同的errorcodes集合

    總結(jié):

    在學(xué)習(xí)的過程中,我們不但要關(guān)注其實現(xiàn)的方式,還要關(guān)注我們能從里面學(xué)到什么?比如說從這個異常抽象中,能學(xué)到幾種設(shè)計模式,以及使用的場景,這些都是可以運用到以后的工作中。

    到此,關(guān)于“Spring不同數(shù)據(jù)庫sql異常的轉(zhuǎn)換方法”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

    向AI問一下細節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI