MyBatis簡單介紹 【1】MyBatis是一個持久層的ORM框架【Object Relational Mapping,對象關係映射】,使用簡單,學習成本較低。可以執行自己手寫的SQL語句,比較靈活。但是MyBatis的自動化程度不高,移植性也不高,有時從一個資料庫遷移到另外一個資料庫的時候需要自 ...
MyBatis簡單介紹
【1】MyBatis是一個持久層的ORM框架【Object Relational Mapping,對象關係映射】,使用簡單,學習成本較低。可以執行自己手寫的SQL語句,比較靈活。但是MyBatis的自動化程度不高,移植性也不高,有時從一個資料庫遷移到另外一個資料庫的時候需要自己修改配置,所以稱只為半自動ORM框架。
傳統JDBC介紹
【1】簡單使用
@Test public void test() throws SQLException { Connection conn=null; PreparedStatement pstmt=null; try { // 1.載入驅動,其實這一步可以不加因為DriverManager裡面會有自動載入驅動的一步 Class.forName("com.mysql.jdbc.Driver"); // 2.創建連接 conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis_example", "root", "123456"); //開啟事務 conn.setAutoCommit(false); // SQL語句 String sql="select id,user_name,create_time from t_user where id=? "; // 獲得sql執行者 pstmt=conn.prepareStatement(sql); pstmt.setInt(1,1); // 執行查詢 //ResultSet rs= pstmt.executeQuery(); pstmt.execute(); ResultSet rs= pstmt.getResultSet(); rs.next(); User user =new User(); user.setId(rs.getLong("id")); user.setUserName(rs.getString("user_name")); user.setCreateTime(rs.getDate("create_time")); System.out.println(user.toString()); } catch (Exception e) { e.printStackTrace(); } finally{ // 關閉資源 try { if(conn!=null){ conn.close(); } if(pstmt!=null){ pstmt.close(); } } catch (SQLException e) { e.printStackTrace(); } } }
【1.1】DriverManager如何自動載入驅動【利用靜態代碼塊調用初始化方法,至於為什麼要使用SPI機制,主要是為了實現解耦和可插拔,因為驅動有多種】
註:驅動展示【位於mysql-connector-java-5.1.22.jar/META-INF/services/java.sql.Driver 內容為:com.mysql.jdbc.Driver】
static { // 啟動類載入器載入DriverManager類,觸發靜態方法執行 loadInitialDrivers(); println("JDBC DriverManager initialized"); } private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // 載入java.sql.Driver驅動的實現 AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { // 1、創建一個 ServiceLoader對象,【這裡就將上下文類載入器設置到ServiceLoader對象的變數上了】 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); // 2、創建一個迭代器對象 Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ // 3、這裡調用driversIterator.hasNext()的時候,觸發將 META-INF/services 下的配置文件中的數據讀取進來,方便下麵的next方法使用 while(driversIterator.hasNext()) { // 4、【關鍵】:觸發上面創建的迭代器對象的方法調用。這裡才是具體載入的實現邏輯,非常不好找 driversIterator.next(); } } catch(Throwable t) {} return null; } }); //判斷有沒有載入到 if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) {...} } } //ServiceLoader類#hasNext方法 public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { // 1、拼湊要讀取的文件的全名 // final String PREFIX = "META-INF/services/"; String fullName = PREFIX + service.getName(); // 2、根據 fullName 去到META-INF/services/目錄下尋找配置文件 // 如果類載入器為空,則使用系統類載入器,如果不為空則使用指定的類載入器 if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } // 3、使用parse方法解析配置文件中的每一行數據 pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; }
【2】當然正常情況一般是封裝成類來使用的,如
//數據訪問基類 public class BaseDao { // 驅動 private static String DRIVER = null; // 鏈接字元串 private static String URL = null; // 用戶名 private static String USERNAME = null; // 密碼 private static String PASSWORD = null; //初始化 static { init(); } // 初始化 private static void init() { try { // 使用Properties對象讀取資源文件屬性 Properties pro = new Properties(); // 獲得資源文件輸入流 InputStream inStream = BaseDao.class.getClassLoader().getResourceAsStream("db.properties"); // 載入輸入流 pro.load(inStream); DRIVER = pro.getProperty("mysql.driverClass"); URL = pro.getProperty("mysql.jdbcUrl"); USERNAME = pro.getProperty("mysql.user"); PASSWORD = pro.getProperty("mysql.password"); Class.forName(DRIVER); } catch (Exception e) { e.printStackTrace(); } } //獲取資料庫連接對象 protected Connection getConnection() { Connection conn = null; try { conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return conn; } /** * 關閉所有鏈接 * @param conn * @param stmt * @param rs */ protected void CloseAll(Connection conn, Statement stmt, ResultSet rs) { try { if (conn != null) { conn.close(); } if (stmt != null) { stmt.close(); } if (rs != null) { rs.close(); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * 執行 增、刪、 改的公共方法 * @param sql SQL語句 * @param prams 參數 * @return 受影響的行數 */ protected int executeUpdate(String sql, Object... prams) { // 獲得資料庫鏈接對象 Connection conn = getConnection(); // 聲明SQL執行者 PreparedStatement pstmt = null; try { // 獲得SQL執行者 pstmt = conn.prepareStatement(sql); // 迴圈載入參數 for (int i = 0; i < prams.length; i++) { pstmt.setObject(i + 1, prams[i]); } // 執行executeUpdate 返回受影響行數 return pstmt.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { // 關閉所有需要關閉的對象 CloseAll(conn, pstmt, null); } return 0; } /** * 執行查詢 返回單個值 * @param sql SQL語句 * @param prams 參數 * @return OBJECT */ protected Object executeScaler(String sql, Object... prams) { // 獲得資料庫鏈接對象 Connection conn = getConnection(); // 聲明SQL執行者 PreparedStatement pstmt = null; // 聲明查詢結果集 ResultSet rs = null; // 接收單個值 Object value = null; try { // 獲得SQL執行者 pstmt = conn.prepareStatement(sql); // 迴圈載入參數 for (int i = 0; i < prams.length; i++) { pstmt.setObject(i + 1, prams[i]); } // 執行executeUpdate 返回受影響行數 rs = pstmt.executeQuery(); if (rs.next()) { value = rs.getObject(1); } } catch (SQLException e) { e.printStackTrace(); } finally { CloseAll(conn, pstmt, rs); } return value; } /** * 執行查詢返回list * * @param sql SQL語句 * @param clazz 類的類型 * @return List */ public <T> List<T> executeList(String sql, Class<T> clazz, Object... prams) { // 數據集合 List<T> list = new ArrayList<T>(); // 獲得資料庫連接 Connection conn = getConnection(); // 聲明SQL執行者 PreparedStatement pstmt = null; // 聲明查詢結果集 ResultSet rs = null; try { // 3. 通過鏈接創建一個SQL執行者 pstmt = conn.prepareStatement(sql); // 迴圈載入參數 for (int i = 0; i < prams.length; i++) { //內部會通過instance of判斷這個參數到底是哪個類型的具體對象 pstmt.setObject(i + 1, prams[i]); } // 4 執行查詢SQL 返回查詢結果 rs = pstmt.executeQuery(); // 獲得結果集的列信息對象 ResultSetMetaData rsmd = rs.getMetaData(); while (rs.next()) { // 通過類反射實例化 T obj = clazz.newInstance(); // 迴圈所有的列 for (int i = 1; i <= rsmd.getColumnCount(); i++) { /* 通過屬性名稱使用反射給泛型實例賦值 Begin */ // 獲得每一列的列名 String cloName = rsmd.getColumnName(i); // 根據列名反射到類的欄位 Field filed = clazz.getDeclaredField(cloName); // 設置私有屬性的訪問許可權 filed.setAccessible(true); // 給泛型實例的某一個屬性賦值 filed.set(obj, rs.getObject(cloName)); /* 通過屬性名稱使用反射給泛型實例賦值 End */ } // 將泛型實例添加到 泛型集合中 list.add(obj); } } catch (Exception e) { e.printStackTrace(); } return list; } /** * 執行查詢返回JavaBean * * @param sql SQL語句 * @param clazz 類的類型 * @return JavaBean */ public <T> T executeJavaBean(String sql, Class<T> clazz, Object... prams) { // 聲明數據對象 T obj=null; // 獲得資料庫連接 Connection conn = getConnection(); // 聲明SQL執行者 PreparedStatement pstmt = null; // 聲明查詢結果集 ResultSet rs = null; try { // 3. 通過鏈接創建一個SQL執行者 pstmt = conn.prepareStatement(sql); // 迴圈載入參數 for (int i = 0; i < prams.length; i++) { pstmt.setObject(i + 1, prams[i]); } // 4 執行查詢SQL 返回查詢結果 rs = pstmt.executeQuery(); // 獲得結果集的列信息對象 ResultSetMetaData rsmd = rs.getMetaData(); if (rs.next()) { // 通過類反射實例化 obj = clazz.newInstance(); // 迴圈所有的列 for (int i = 1; i <= rsmd.getColumnCount(); i++) { /* 通過屬性名稱使用反射給泛型實例賦值 Begin */ // 獲得每一列的列名 String cloName = rsmd.getColumnName(i); // 根據列名反射到類的欄位 Field filed = clazz.getDeclaredField(cloName); // 設置私有屬性的訪問許可權 filed.setAccessible(true); // 給泛型實例的某一個屬性賦值 filed.set(obj, rs.getObject(cloName)); /* 通過屬性名稱使用反射給泛型實例賦值 End */ } } } catch (Exception e) { e.printStackTrace(); } return obj; } }
【3】總結JDBC的四大核心對象
1)DriverManager(驅動管理對象):獲取資料庫連接;
2)Connection(資料庫連接對象):獲取執行sql對象;管理事務;
3)Statement(執行sql對象):executeUpdate執行DML語句(增刪改)DDL語句;executeQuery 執行DQL語句;
4)ResultSet(結果集對象):
【4】傳統JDBC的問題
1)資料庫連接創建,釋放頻繁造成西戎資源的浪費,從而影響系統性能,使用資料庫連接池可以解決問題。
2)sql語句在代碼中硬編碼,造成代碼的不已維護,實際應用中sql的變化可能較大,sql代碼和java代碼沒有分離開來維護不方便。
3)使用preparedStatement向有占位符傳遞參數存在硬編碼問題因為sql中的where子句的條件不確定,同樣是修改不方便。
4)對結果集中解析存在硬編碼問題,sql的變化導致解析代碼的變化,系統維護不方便。
【5】針對問題的優化
1、資料庫連接創建、釋放頻繁造成系統資源浪費從而影響系統性能,如果使用資料庫連接池可解決此問題。
優化部分,如mybatis:在SqlMapConfig.xml中配置數據連接池,使用連接池管理資料庫鏈接。
2、Sql語句寫在代碼中造成代碼不易維護,實際應用sql變化的可能較大,sql變動需要改變java代碼。
優化部分,如mybatis:將Sql語句配置在XXXXmapper.xml文件中與java代碼分離。
3、向sql語句傳參數麻煩,因為sql語句的where條件不一定,可能多也可能少,占位符需要和參數一一對應。
優化部分,如mybatis:自動將java對象映射至sql語句,通過statement中的parameterType定義輸入參數的類型。
4、對結果集解析麻煩,sql變化導致解析代碼變化,且解析前需要遍歷,如果能將資料庫記錄封裝成pojo對象解析比較方便。
優化部分,如mybatis:自動將sql執行結果映射至java對象,通過statement中的resultType定義輸出結果的類型。
Mybaits整體體系圖
【1】圖示
【2】分析
把Mybatis的功能架構分為三層:
1)基礎支撐層:負責最基礎的功能支撐,包括連接管理、事務管理、配置載入和緩存處理,這些都是共用的東西,將他們抽取出來作為最基礎的組件。為上層的數據處理層提供最基礎的支撐。
對應示例中的部分為: SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
2)API介面層:提供給外部使用的介面API,開發人員通過這些本地API來操縱資料庫。介面層一接收到調用請求就會調用數據處理層來完成具體的數據處理。
對應示例中的部分為: SqlSession session = sqlMapper.openSession();
3)數據處理層:負責具體的SQL查找、SQL解析、SQL執行和執行結果映射處理等。它主要的目的是根據調用的請求完成一次資料庫操作。
對應示例中的部分為:
User user = (User)session.selectOne("com.mapper.UserMapper.selectById", 1);
或者
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1L);
【3】簡單示例
public static void main(String[] args) { String resource = "mybatis-config.xml"; Reader reader; try { //將XML配置文件構建為Configuration配置類 reader = Resources.getResourceAsReader(resource); // 通過載入配置文件流構建一個SqlSessionFactory DefaultSqlSessionFactory SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader); // 數據源 執行器 DefaultSqlSession SqlSession session = sqlMapper.openSession(); try { // 執行查詢 底層執行jdbc //User user = (User)session.selectOne("com.mapper.UserMapper.selectById", 1); UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.selectById(1L); session.commit(); System.out.println(user.getUserName()); } catch (Exception e) { e.printStackTrace(); }finally { session.close(); } } catch (IOException e) { e.printStackTrace(); } }
Mybaits插件的分析
【1】插件主要作用於四⼤組件對象
1)執⾏器 Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed 等⽅法)
2)參數處理器 ParameterHandler (getParameterObject, setParameters 等⽅法)
3)結果集處理器 ResultSetHandler (handleResultSets, handleOutputParameters 等⽅法)
4)SQL語法構建處理器 StatementHandler (prepare, parameterize, batch, update, query 等⽅法)
源碼分析部分
MyBatis解析全局配置文件的源碼分析
【1】分析怎麼通過載入配置文件流構建一個SqlSessionFactory
public SqlSessionFactory build(Reader reader) { return build(reader, null, null); } public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } // 到這裡配置文件已經解析成了Configuration public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
【1.1】分析XMLConfigBuilder類怎麼將xml的資源文件流解析成Configuration:
【1.1.1】新建XMLConfigBuilder過程【所以說XMLConfigBuilder並不負責解析,解析的是它裡面的XPathParser類,】
/** * 創建一個用於解析xml配置的構建器對象 * @param inputStream 傳入進來的xml的配置 * @param environment 我們的環境變數 * @param props:用於保存我們從xml中解析出來的屬性 */ public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) { /** * 該方法做了二個事情 * 第一件事情:創建XPathParser 解析器對象,在這裡會把我們的 * 把我們的mybatis-config.xml解析出一個Document對象 * 第二節事情:調用重寫的構造函數來構建我XMLConfigBuilder */ this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props); } private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { //調用父類的BaseBuilder的構造方法:給configuration賦值,typeAliasRegistry別名註冊器賦值,TypeHandlerRegistry賦值 super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); //把props綁定到configuration的props屬性上 this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; } //又因為class XMLConfigBuilder extends BaseBuilder,所以看一下他的父類 public abstract class BaseBuilder { // mybatis的全局配置文件 protected final Configuration configuration; // 用於保存我們的Entity的別名 protected final TypeAliasRegistry typeAliasRegistry; // 用戶保存我們java類型和jdbc資料庫類型的 protected final TypeHandlerRegistry typeHandlerRegistry; public BaseBuilder(Configuration configuration) { this.configuration = configuration; this.typeAliasRegistry = this.configuration.getTypeAliasRegistry(); this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); } .... }
【1.1.2】parser.parse()解析過程【本質上就是從根結點開始解析】
public Configuration parse() { //若已經解析過了 就拋出異常 if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } //設置解析標誌位,保證只解析一次 parsed = true; /** * 解析我們的mybatis-config.xml的節點 * <configuration> </configuration> */ parseConfiguration(parser.evalNode("/configuration")); return configuration; } //方法實現說明:解析我們mybatis-config.xml的 configuration節點 private void parseConfiguration(XNode root) { try { // 解析 properties節點 // 如:<properties resource="mybatis/db.properties" /> propertiesElement(root.evalNode("properties")); // 解析我們的mybatis-config.xml中的settings節點 // 如:<setting name="mapUnderscoreToCamelCase" value="false"/> Properties settings = settingsAsProperties(root.evalNode("settings")); // 基本沒有用過該屬性 // VFS含義是虛擬文件系統;主要是通過程式能夠方便讀取本地文件系統、FTP文件系統等系統中的文件資源。 // Mybatis中提供了VFS這個配置,主要是通過該配置可以載入自定義的虛擬文件系統應用程式 loadCustomVfs(settings); // 指定 MyBatis 所用日誌的具體實現,未指定時將自動查找。 // SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING // 解析到org.apache.ibatis.session.Configuration#logImpl loadCustomLogImpl(settings); // 解析別名typeAliases節點 typeAliasesElement(root.evalNode("typeAliases")); // 解析插件節點(比如分頁插件),解析到 interceptorChain.interceptors pluginElement(root.evalNode(