第33次文章:SORM框架(三)
- 2019 年 10 月 8 日
- 筆記
本周將SORM框架的基本功能已經全部填充起來了,形成了SORM框架的1.0版本,有興趣的同學可以通過下面的鏈接獲取源碼喲!下周將進入SORM框架的升級階段,在現在1.0版本的基礎上,加入設計模式等內容,使得這個架構具有更強的可擴性。
下面我們結合這段SORM框架源碼以及基本用法進行一個相關介紹。
在我們日常使用資料庫時,一般的操作為增、刪、改、查。我們通過對4種操作的分析,可以將增刪改歸為java對象到資料庫的操作,而查詢操作可以將其歸為資料庫到java對象的操作。於是,我們根據數據在java和資料庫中的傳遞方向,將所有操作分為了兩大類進行講解。
一、從java對象到資料庫的操作
從java對象到資料庫有增刪改三類操作,雖然功能不同,但是在實現過程中,有一些基本的思路是相通的。為了簡化用戶的操作,我們一般都希望可以通過向三種方法中傳遞被操作的java對象,然後使對象和資料庫之間產生相應的聯繫,最終改變資料庫中存儲的數據。
1.準備工作
在具體的實現的時候,我們遇到的一個問題:如何將傳遞的java對象與資料庫中的表進行對應?
解決方案一:我們在對表格以及java類進行命名的時候,遵循了一個基本原則,java類的名稱與表格中各類名稱只有首字母的大小寫不相同,其餘的部分均相同,我們如果利用這種原則,可以通過字元串的匹配進行判斷,選擇每個java類對應的表格,但是顯然會比較繁瑣。
解決方案二:我們在根據資料庫中的資訊生成po包中的各個java類的時候,我們可以將每個類和表進行關聯,存儲在一個Map中。這樣就可以在我們利用java類尋找關聯表格的時候節省大量的時間,提高效率。
在我們的實踐中,採取的就是解決方案二。在TableContex類中增設了updateJavaPOFile()方法,用於根據表結構來更新po包中的java類,以及loadPoTables()方法,用於載入po包的類,將其與表格進行關聯。
經過上面的基礎準備之後,我們對每個傳入的java類對象進行操作的基本思想為:首先從給出的對象轉換到class類,根據class類獲取與之對應的表。然後根據class中的屬性名等,開始拼接sql字元串。最後在java中執行sql語句,改變資料庫中的數據內容。
2.excuteDML方法
由於增刪改都是需要在java中執行mysql的語句來對資料庫進行操作,每一種操作最後都需要執行sql語句,所以我們可以單獨封裝一個執行sql語句的方法,便於後續的重用。方法的源碼如下所示:
/** * 直接執行一個DML語句 * @param sql sql語句 * @param params 參數 * @return 執行sql語句後影響記錄的行數 */ @Override public int excuteDML(String sql, Object[] params) { Connection conn = DBManager.getConn(); int count = 0; PreparedStatement ps = null; try { ps = (PreparedStatement) conn.prepareStatement(sql); JDBCUtils.handleParams(ps, params); System.out.println(ps); count = ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { DBManager.close(ps, conn); } return count; }
tips:在增刪改三種操作中,每一種操作執行sql語句的過程都是相同的,不同的僅僅是sql語句以及傳遞的參數而已,所以當我們封裝好excuteDML方法之後,我們就可以在增刪改當中,專註於sql語句的拼接即可,提高效率。
3.插入操作
下面我們介紹一下插入操作insert方法,源碼如下:
/** * 將一個對象存儲到資料庫中 * 把對象中不為null的屬性往資料庫中存儲!如果數字為null則放0. * @param object 要存儲的對象 */ @Override public void insert(Object obj) { //obj--->表中。insert into 表名 (id,uname,pwd) values (?,?,?) Class clazz = obj.getClass(); TableInfo tableInfo = TableContext.poClassTableMap.get(clazz); StringBuilder sql = new StringBuilder( "insert into "+tableInfo.getTname()+" ("); int countNotNullField = 0;//計算不為null的屬性值 List<Object> params = new ArrayList<Object>();//存儲sql的參數對象 //獲取所有不為空的屬性 Field[] fs = clazz.getDeclaredFields(); for(Field f:fs) { String fieldName = f.getName();//獲取屬性名 Object fieldValue = ReflectUtils.invokeGet(fieldName, obj);//獲取屬性對應的值 if(fieldValue != null) {//檢查屬性值是否為空 countNotNullField++; sql.append(fieldName+","); params.add(fieldValue); } } sql.setCharAt(sql.length()-1, ')');//注意,在java中,單引號之間的字母被識別為char,雙引號之間的字母被識別為Sring sql.append(" values ("); for(int i=0;i<countNotNullField;i++) { sql.append("?,"); } sql.setCharAt(sql.length()-1, ')'); excuteDML(sql.toString(), params.toArray()); }
tips:在插入一個對象的時候,該對象就代表著表格中的一行記錄。
我們首先需要知道此對象中,各個屬性的值,也就是我們需要向sql語句中傳遞的參數列表,所以需要將對象中不為null的屬性值獲取出來,然後對屬性名稱拼接sql字元串,最後調用excuteDML方法,向方法中傳入拼接好的sql語句,以及對應的參數數組params即可。
4.刪除操作
刪除操作也和上面的程式碼類似,主要就是拼接刪除操作指令。
/** * 刪除clazz表示類對應的表中的記錄(指定主鍵值id的記錄) * @param clazz * @param id */ @Override public void delete(Class clazz, Object id) { //Emp.class,2 ----> delete from emp where id=2 //通過Class對象找TableInfo TableInfo tableInfo = TableContext.poClassTableMap.get(clazz); //獲得主鍵 ColumnInfo onlyPriKey = tableInfo.getOnlyPriKey(); String sql = "delete from "+tableInfo.getTname()+" where "+onlyPriKey.getName()+"=?"; excuteDML(sql, new Object[] {id}); } @Override public void delete(Object obj) { Class clazz = obj.getClass();//獲取class對象 TableInfo tableInfo = TableContext.poClassTableMap.get(clazz);//獲取對象對應的表資訊 ColumnInfo onlyPriKey = tableInfo.getOnlyPriKey();//主鍵 //通過反射機制,調用屬性對應的get方法或set方法 Object priKeyValue = ReflectUtils.invokeGet(onlyPriKey.getName(), obj); delete(clazz,priKeyValue); }
tips:在刪除過程中,我們首先根據需要刪除的對象,獲取表中的主鍵,因為在刪除時,我們只有根據主鍵的值來作為根據,才不會誤刪其他記錄。所以最後向excuteDML方法中傳遞的參數即為需要刪除的主鍵的值。在我們現階段編寫的SORM框架中,我們只支援資料庫中為一個主鍵的情況。如果考慮到聯合主鍵,那麼情況將會較為複雜,留給以後考慮。
5.更改操作
方法如下:
@Override public int update(Object obj, String[] fieldNames) { //obj{"uname","pwd"}----> update 表名 set uname=?,pwd=? where id=? Class clazz = obj.getClass(); TableInfo tableInfo = TableContext.poClassTableMap.get(clazz); StringBuilder sql = new StringBuilder("update "+tableInfo.getTname()+" set "); List<Object> params = new ArrayList<Object>(); ColumnInfo priKey = tableInfo.getOnlyPriKey();//獲取主鍵 for(String fieldName:fieldNames) { params.add(ReflectUtils.invokeGet(fieldName, obj)); sql.append(fieldName+"=?,"); } sql.setCharAt(sql.length()-1, ' '); sql.append("where "+priKey.getName()+"=? "); params.add(ReflectUtils.invokeGet(priKey.getName(),obj));//加入主鍵的值 return excuteDML(sql.toString(), params.toArray()); }
tips:更改操作和刪除具有一點相像之處,主要在於兩者都是依賴對象的主鍵來對資料庫操作。在更改操作中,我們傳入需要更改的屬性名稱,在對象obj中獲取對應的屬性值,最後再拼接sql語句字元串,執行更新操作。
二、從資料庫到java對象的操作
從資料庫中查詢操作,由於查詢的內容都是屬於外部傳輸,所以我們直接向方法中提供sql語句以及相關的參數即可。
整個方法的基本思路為:首先與資料庫進行連接,獲取連接connection對象,然後通過查詢語句返回查詢的結果,最後將查詢得到的結果封裝在用戶需要使用的類中。
1.多行多列查詢操作
對於查詢,有時候會涉及到查詢得到的結果是多個對象的多個屬性值,面對這樣的情況,我們需要按照行和列的不同維度去封裝每一個返回對象結果。
/** * 查詢返回多行記錄,並將每行記錄封裝到clazz指定的類的對象中 * @param sql 查詢語句 * @param clazz 封裝數據的javabean類的class對象 * @param params sql的參數 * @return 查詢到的結果 */ @Override public List queryRows(String sql, Class clazz, Object[] params) { Connection conn = DBManager.getConn(); List list = null; //存儲查詢結果的容器 PreparedStatement ps = null; ResultSet rs = null; try { ps = (PreparedStatement) conn.prepareStatement(sql); //給sql設置參數 JDBCUtils.handleParams(ps, params); System.out.println(ps); rs = ps.executeQuery(); ResultSetMetaData metaData = rs.getMetaData(); //多行 while(rs.next()) { if(list==null) { list = new ArrayList(); } Object rowObj = clazz.newInstance(); //調用Javabean的無參構造器 //多列 select username,pwd,age from user where id>? and age>18 for(int i=0;i<metaData.getColumnCount();i++) { String columnName = metaData.getColumnLabel(i+1); Object columnValue = rs.getObject(i+1); //調用rowObject對象的setUsername方法,將ColumnValue的值設置進去 ReflectUtils.invokeSet(rowObj, columnName, columnValue); } list.add(rowObj); } } catch (SQLException e) { e.printStackTrace(); } catch (InstantiationException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { DBManager.close(ps, conn); } return list; }
tips:在整個查詢的過程中,我們使用兩個循環來分別封裝每個查詢到的對象。多行對應著多個對象,多列對應著每個對象的不同的屬性。將查詢到的每個屬性封裝到提前創建好的類中,把所得每個類加入到鏈表中,最後返回鏈表。
2.查詢其他類型
對應於多行多列的查詢,還有一行多列,和一行一列,以及單獨返回一個數字。
/** * 查詢返回一行記錄,並將每行記錄封裝到clazz指定的類的對象中 * @param sql 查詢語句 * @param clazz 封裝數據的javabean類的class對象 * @param params sql的參數 * @return 查詢到的結果 */ @Override public Object queryUniqueRow(String sql, Class clazz, Object[] params) { List list = queryRows(sql, clazz, params); return (list==null&&list.size()>0)?null:list.get(0); } /** * 查詢返回一個值(一行一列),並將該值返回 * @param sql 查詢語句 * @param params sql的參數 * @return 查詢到的結果 */ @Override public Object queryValue(String sql, Object[] params) { Connection conn = DBManager.getConn(); Object value = null; //存儲查詢結果的對象 PreparedStatement ps = null; ResultSet rs = null; try { ps = (PreparedStatement) conn.prepareStatement(sql); //給sql設置參數 JDBCUtils.handleParams(ps, params); System.out.println(ps); rs = ps.executeQuery(); while(rs.next()) { value = rs.getObject(1); } } catch (SQLException e) { e.printStackTrace(); } finally { DBManager.close(ps, conn); } return value; } /** * 查詢返回一個數字(一行一列),並將該值返回 * @param sql 查詢語句 * @param params sql的參數 * @return 查詢到的數字 */ @Override public Number queryNumber(String sql, Object[] params) { return (Number)queryValue(sql,params);//查詢一個數字,返回對象 }
tips:當我們將多行多列的查詢實現之後,剩餘的幾種方法實現起來都較為容易。對於一行多列的情況,我們可以直接調用多行多列的方法queryRows(),從返回得到的list取出唯一的對象即可。
3.簡單的測試一下查詢方法的效果
public static void main(String[] args) { List<Emp> list = new MySqlQuery().queryRows("select id,empname,age,birthday from emp where age>? and salary<?", Emp.class, new Object[] {10,10000}); for(Emp e:list) { System.out.println(e.getEmpname()+" "+e.getAge()+" "+e.getId()+" "+e.getBirthday()); System.out.println("******************"); } //複雜查詢,將兩個表進行關聯查詢 String sql2 = "select e.id,e.empname,salary+bonus 'xinshui',age,d.dname 'deptName',d.address 'deptAddr' from emp e " +"join dept d on e.deptId=d.id "; List<EmpVO> list2 = new MySqlQuery().queryRows(sql2,EmpVO.class, null); for(EmpVO e:list2) { System.out.println(e.getEmpname()+" "+e.getAge()+" "+e.getId()+" "+e.getXinshui()+" "+e.getDeptName()+" "+e.getDeptAddr()); System.out.println("******************"); } }
所得結果:

tips:在上面這段測試程式碼中,我們進行了兩次查詢。我們關注一下後面的這個複雜查詢。
我們的資料庫中有兩張表格,分別為dept和emp表格,兩張表格的分別如下所示:


可以很明顯的看到一點,在emp和dept可以通過deptId進行關聯。當我們單獨查詢emp表格的時候,無法顯示出每個僱員的辦公地址。當我們使用複雜查詢,聯合兩張表格的時候,我們需要重新定義一個可以封裝查詢到的各個屬性數據的類。所以我們根據自己的需要,重新定義了一個EmpVO類。在此類中,加入了需要封裝的屬性,比如emp中的Name屬性,以及dept中的addr屬性等等。最後將所得結果輸出到控制台上,得到上圖顯示出來的結果。