手把手教你写一个Java的orm框架(4)
開始準(zhǔn)備生成sql
在上一篇里,我們已經(jīng)取到了我們在生成sql語句中所需要的信息,這一篇里我們開始根據(jù)class來生成我們需要的sql。在這之前我們先確認(rèn)幾件事情
sql里的參數(shù)我們使用占位符的形式。
這里用的是jdbc中的PreparedStatement,sql中的參數(shù)使用“?”的形式。
大致上是這樣的:
Connection connection = dataSource.getConnection(); PreparedStatement preparedStatement = connection.prepareStatement("select * from `user` where `status` = ? ;"); preparedStatement.setObject(1, 0); ResultSet resultSet = preparedStatement.executeQuery(); 復(fù)制代碼但是這樣的話我們每次執(zhí)行都需要手寫這些執(zhí)行sql的繁瑣的代碼,我在這里選擇使用spring-jdbc中的JdbcTemplte。這樣我就只需要生成sql,然后使用JdbcTemplte里的方法來執(zhí)行sql就好了。
我們只生成單表的增刪改查,不涉及復(fù)雜sql。
不貼出完整的代碼,以說明思路為主。
畢竟這個是已經(jīng)寫好的代碼,地址在:github.com/hjx60149632… 。所有代碼可以在這里找到。
分析sql
我們主要解決的是增刪該查的問題,所以我們先寫如何生成一個新增的sql。
我么先觀察一下sql一般來說都有什么構(gòu)成。現(xiàn)在先放一個例子出來:
insert
INSERT INTO user (name, id, create_date, age, mark, status) VALUES (?, ?, ?, ?, ?, ?); 復(fù)制代碼delete
DELETE FROM user WHERE id = ? 復(fù)制代碼update
UPDATE user SET name = ?,id = ?,create_date = ?,age = ?,status = ? WHERE id = ? 復(fù)制代碼select
SELECT name, id, create_date, age, mark, status FROM user WHERE id = ? 復(fù)制代碼通過觀察上面的sql,可以發(fā)現(xiàn)其中有一些共性:
接下來,就可以按照每種類型的sql來創(chuàng)建sql了。
操作對象
一下所有的對象都是這個User.java
import javax.persistence.Column; import javax.persistence.Id; import javax.persistence.Table; import java.util.Date;(name = "user") public class User {(name = "name")private String name;(name = "id")private int id;(name = "age")private int age;(name = "mark")private String mark;(name = "create_date")private Date createDate;(name = "status")private int status;// getter setter toString } 復(fù)制代碼先寫點工具代碼
主要用來操作字符串
import java.util.Collection; import java.util.Iterator;/*** @author hjx*/ public class StringUtils {public static final String SPACE = " ";public static final String BLANK = "";public static final String COMMA = ", ";/*** 重復(fù)字符串** @param str* @param number* @return*/public static String[] repeat(String str, int number) {Assert.notNull(str);String[] strings = new String[number];for (int i = 0; i < number; i++) {strings[i] = str;}return strings;}/*** 組合字符串** @param strings* @return*/public static String append(final Object... strings) {StringBuilder builder = new StringBuilder();for (Object s1 : strings) {if (s1 == null) {continue;}builder.append(s1.toString());}return builder.toString();}/*** 組合字符串** @param collection* @param separator* @return*/public static String join(Collection collection, String separator) {StringBuffer var2 = new StringBuffer();for (Iterator var3 = collection.iterator(); var3.hasNext(); var2.append((String) var3.next())) {if (var2.length() != 0) {var2.append(separator);}}return var2.toString();} }復(fù)制代碼用來從對象中取值的,使用反射。
/*** 取值** @param target 要從哪一個對象中取值* @param field 要取這個對象的那個屬性的值* @return*/ public static Object getValue(Object target, Field field) {//忽略掉privatefield.setAccessible(true);try {return field.get(target);} catch (IllegalAccessException e) {e.printStackTrace();}return null; } 復(fù)制代碼? 用來給對象設(shè)置值的,還是反射。
/*** 設(shè)置值** @param target 要從哪一個對象中取值* @param field 要取這個對象的那個屬性的值* @param value 要設(shè)置的值* @return*/ public static boolean setValue(Object target, Field field, Object value) {field.setAccessible(true);try {field.set(target, value);return true;} catch (IllegalAccessException e) {e.printStackTrace();}return false; } 復(fù)制代碼下面就可以開始創(chuàng)建各種sql了~~~
生成sql:insert
思路
新增的sql還是比較好實現(xiàn)的,我們需要的大致就是:
實現(xiàn)
首先我們要根據(jù)User.java拿到所有的表的字段個名稱,和對應(yīng)的值。就是上一篇寫到的:EntityTableRowMapper
拿到字段和class屬性的值
Map<String, Field> columnFieldMapper = entityTableRowMapper.getColumnFieldMapper(); insertColumns = new ArrayList(columnFieldMapper.size()); for (Map.Entry<String, Field> stringFieldEntry : columnFieldMapper.entrySet()) {Field field = stringFieldEntry.getValue();Object value = EntityUtils.getValue(entity, field);if (value == null) {continue;}insertColumns.add(stringFieldEntry.getKey());insertColumnValues.add(value); } 復(fù)制代碼這里有兩個變量:
insertColumns:sql中的字段名。
insertColumnValues:sql中的字段對應(yīng)的值。
生成插入的sql:
StringBuilder builder = new StringBuilder(); int size = insertColumns.size(); builder.append("INSERT INTO ").append(getTableName()).append(StringUtils.SPACE); builder.append(StringUtils.append("( ", StringUtils.join(insertColumns, ", "), " ) ")); builder.append("VALUES "); for (int i = 0; i < insertCount; i++) {builder.append("( ");String[] repeat = StringUtils.repeat("?", size);builder.append(StringUtils.join(Arrays.asList(repeat), ", "));builder.append(" )");if (i != insertCount - 1) {builder.append(StringUtils.COMMA);} } builder.append(";"); 復(fù)制代碼生成的結(jié)果:
//user User user = new User(); user.setId(10); user.setCreateDate(new Date()); user.setAge(20); user.setMark("ceshi"); user.setName("heiheihei"); //sql INSERT INTO user ( name, id, create_date, age, mark, status ) VALUES ( ?, ?, ?, ?, ?, ? ); //value [heiheihei, 10, Tue Jan 22 16:33:00 CST 2019, 20, ceshi, 0] 復(fù)制代碼現(xiàn)在可以拿著生成的sql和值去執(zhí)行啦~
jdbcTemplate.update(sql, insertColumnValues.toArray()); 復(fù)制代碼生成sql:where
上一篇里我們實現(xiàn)了生成insert的sql,下面要開始實現(xiàn)update,delete,select的sql語句了。但是這些語句有一個比較麻煩的地方是:它們一般后面都會有where條件,因為在執(zhí)行的時候不能把表里所有的數(shù)據(jù)都進(jìn)行操作。
所以這里我們需要先生成條件的sql。大概是這樣的:
WHERE id = ? AND name != ? OR age >= ? 復(fù)制代碼where 后面的參數(shù)繼續(xù)用 “?” 代替。值就放在一個有序的集合中就好了。類似上一篇提到的insertColumnValues。
思路
實現(xiàn)
第一步
我們實現(xiàn)第一步,在這之前我們先看一下一個條件是有什么組成的,例如:
1: id = ? AND 2: name != ? OR 3: age >= ? 復(fù)制代碼這里通過觀察可以發(fā)現(xiàn),每一個條件都是由一個 字段名稱,一個判斷,**一個占位符 "?"**和后面用于連接條件的 AND 或者 OR 所構(gòu)成。這樣我們可以編寫一個類用來保存這些信息:
Where.java
import java.util.ArrayList; import java.util.Arrays; import java.util.List;/*** where條件 默認(rèn)使用 and 連接多個條件** @author hjx*/ public class Where {protected static final String PLACEHOLDER = "#{COLUMN}";static final String AND = "AND ";static final String OR = "OR ";private String sql;private String column;private String connect = AND;private List<Object> values;/*** 是否有值(null 也代表有值)*/private boolean hasValue;/*** @param column 被操作的列* @param sql 操作的sql*/public Where(String column, String sql) {this.column = column;this.sql = sql;this.hasValue = false;this.values = new ArrayList<>();}/*** @param column 被操作的列* @param sql 操作的sql* @param value sql的參數(shù)*/public Where(String column, String sql, Object value) {this.sql = sql;this.column = column;this.values = new ArrayList<>();this.values.add(value);this.hasValue = true;}/*** @param column 被操作的列* @param sql 操作的sql* @param values sql的參數(shù)*/public Where(String column, String sql, Object[] values) {this.sql = sql;this.column = column;this.values = Arrays.asList(values);this.hasValue = true;}public Where or() {this.connect = OR;return this;}public Where and() {this.connect = AND;return this;}/*** 獲取本次條件的連接符** @return*/public String getConnect() {return connect;}protected String getSql() {return sql;}protected boolean isHasValue() {return hasValue;}protected List<Object> getValues() {return values;}public String getColumn() {return column;} } 復(fù)制代碼上面中的常量 PLACEHOLDER 是作為一個占位符使用的,下面會說道。
這樣,一個用于保存單個條件的類就寫好了,在一個sql中有多個條件的話,只需要用一個ArrayList保存這些條件,并按照一定的條件拼裝成sql就好了。
第二步
sql中還有一些比較常用的判斷,比如:!= , = , <= , >= 等等,我們在這里可以創(chuàng)建一個工具類來快速的生成Where 這個類,可以這樣寫:
Wheres.java
import java.util.Arrays;/*** 查詢條件* @author hjx*/ public class Wheres {public static Where equal(final String columnName, final Object value) {return new Where(columnName, Where.PLACEHOLDER + " = ? ", value);}public static Where notEqual(final String columnName, final Object value) {return new Where(columnName, Where.PLACEHOLDER + " != ? ", value);}public static Where not(final String columnName, final Object value) {return new Where(columnName, Where.PLACEHOLDER + " <> ? ", value);}public static Where isNotNull(final String columnName) {return new Where(columnName, Where.PLACEHOLDER + " IS NOT NULL ");}public static Where isNull(final String columnName) {return new Where(columnName, Where.PLACEHOLDER + " IS NULL ");}public static Where greater(final String columnName, final Object value, final boolean andEquals) {if (andEquals) {return new Where(columnName, Where.PLACEHOLDER + " >= ? ", value);}return new Where(columnName, Where.PLACEHOLDER + " > ? ", value);}public static Where less(final String columnName, final Object value, final boolean andEquals) {if (andEquals) {return new Where(columnName, Where.PLACEHOLDER + " <= ? ", value);}return new Where(columnName, Where.PLACEHOLDER + " < ? ", value);}public static Where like(final String columnName, final Object value) {return new Where(columnName, Where.PLACEHOLDER + " like ? ", value);}public static Where betweenAnd(final String columnName, final Object value1st, final Object value2nd) {return new Where(columnName, Where.PLACEHOLDER + " between ? and ? ", new Object[]{value1st, value2nd});}public static Where in(final String columnName, final Object[] values) {Object[] sqlVal = values;if (sqlVal.length == 0) {sqlVal = new Object[]{null};}StringBuffer inSql = new StringBuffer();inSql.append(Where.PLACEHOLDER);inSql.append(" IN ( ");String[] strings = StringUtils.repeat("?", sqlVal.length);inSql.append(StringUtils.join(Arrays.asList(strings), ", "));inSql.append(" ) ");return new Where(columnName, inSql.toString(), sqlVal);}} 復(fù)制代碼這里只是簡單的列出了一些常用的判斷條件,如果有特殊需要的自己再加進(jìn)去就好了。
關(guān)于常量 PLACEHOLDER 是這么一回事:
在生成sql 的時候,我需要做一些字段上的驗證。這里在sql中使用一個占位符放進(jìn)sql中,真正參與條件的字段放在另外一個屬性中保存。這樣在真正生成sql的時候可以驗證條件中的字段在不在表中,如果存在的話將字段和占位符進(jìn)行替換就好了。并且如果使用的是屬性名稱的話,也可以根據(jù)名稱找到對應(yīng)的表的字段名。
第三步
通過上面的代碼,我們可以很方便的創(chuàng)建條件了。現(xiàn)在我們將這些條件組裝成我們需要的完整的sql。
注意:這里的代碼可能和我的github上的不太一樣,因為這里只講一下思路,具體的怎么將所有的代碼組裝起來讓它成為一個完整的項目,每個人都不一樣。所以~~~ 嘿嘿。
現(xiàn)在開始:
我們還是以之前寫的User.java為例子
List<Where> wheres = Arrays.asList(Wheres.equal("name", "李叔叔"),Wheres.notEqual("status", 1),Wheres.in("age", new Integer[]{1, 2, 3, 4, 5}),Wheres.greater("age", 20, true) ); List<Object> sqlValue = new ArrayList<>(); StringBuilder sql = new StringBuilder(); if (wheres.size() != 0) {sql.append("WHERE ");for (int i = 0; i < wheres.size(); i++) {Where where = wheres.get(i);if (i != 0) {sql.append(where.getConnect());}String column = where.getColumn();String whereSql = where.getSql();sql.append(//這里獲取真實sqlwhereSql.replace(Where.PLACEHOLDER, getColumnName(column)));//因為有些條件中的參數(shù)可能是有多個List<Object> values = where.getValues();for (int j = 0; j < values.size(); j++) {sqlValue.add(values.get(j));}} } System.out.println(sql.toString()); System.out.println(sqlValue.toString()); 復(fù)制代碼這里說明一下:getColumnName(String name) ,這個方法是根據(jù)參數(shù)獲取真正的字段名稱的方法。因為這個條件中可能傳入的是java屬性的名稱而不是表的字段名稱,需要轉(zhuǎn)換成為真正的表的字段名。這一步也是從之前生成的映射中獲取的。順便還能驗證一下表中有沒有這個字段。這個方法我就不貼出來了,github上有。
輸出結(jié)果:
WHERE name = ? AND status != ? AND age IN ( ?, ?, ?, ?, ? ) AND age >= ? [李叔叔, 1, 1, 2, 3, 4, 5, 20] 復(fù)制代碼這里一個where就寫好了,并且也可以拿到條件中的參數(shù)了。
剩下的就是后面的單獨生成update,delete,select 類型sql的操作了。
生成sql:select
上一篇講了怎樣生成一個sql中where的一部分,之后我們要做事情就簡單很多了,就只要像最開始一樣的生成各種sql語句就好了,之后只要再加上我們需要的條件,一個完整的sql就順利的做好了。
現(xiàn)在我們開始寫生成查詢語句的sql。一個查詢語句大致上是這樣的:
SELECT name, id, create_date, age, mark, status FROM user 復(fù)制代碼這里可以看出來,一個基礎(chǔ)的查詢語句基本上就是一個 SELECT 后面加上需要查詢的字段,跟上 FROM 和要查詢的表名稱就好了。 最多后面可能需要加上 ORDER BY/GROUP BY/LIMIT ....之類的就好了,因為比較簡單,這里就不寫了。(太復(fù)雜的就直接寫sql就好了,我自己不需要這種操作)
思路
這幾步都還是比較好做的,第一步很簡單,仿照著之前寫的就可以了。因為這里在執(zhí)行sql的時候,我使用的是JdbcTemplate,這里有一個不大不小的坑,下面我說一下。
一個不大不小的坑
這個坑是我在使用我寫好的這個項目給公司做報表的時候碰到的。原因是這樣,因為數(shù)據(jù)庫中有些字段是datetime類型的,這個字段有時候在表中的值是:0000-00-00 00:00:00,(我也不知道這個值是怎么進(jìn)去的,但是就是存在/(ㄒoㄒ)/~~)但是這個值是無法轉(zhuǎn)換成為java中的Date類型。所以這里會報錯。
我在這里寫了一個繼承SpringJdbc中的ColumnMapRowMapper的類,是這樣的:
import org.springframework.jdbc.core.ColumnMapRowMapper; import java.sql.ResultSet; import java.sql.SQLException;/*** 捕獲取值的錯誤** @author hjx*/ public class PlusColumnMapRowMapper extends ColumnMapRowMapper {/*** 數(shù)據(jù)庫類型為時間時, 如果值為 0000-00-00 00:00:00* 會報錯,所以重寫此方法,返回null** @param rs* @param index* @return* @throws SQLException*/protected Object getColumnValue(ResultSet rs, int index) throws SQLException {Object columnValue = null;try {columnValue = super.getColumnValue(rs, index);} catch (SQLException e) {e.printStackTrace();}return columnValue;} } 復(fù)制代碼這個類具體在哪里使用,會在下面說明。
實現(xiàn)
現(xiàn)在說一下怎么實現(xiàn)上面的思路,首先因為第一步比較簡單,就不寫了。我直接從第二步開始。
執(zhí)行sql,并取出結(jié)果。
這里我用的是JdbcTemplate的方法,這給我們提供了一個方法:
<T> List<T> query(String sql, Object[] args, RowMapper<T> rowMapper) 復(fù)制代碼這里前兩個參數(shù)比較好理解,一個是sql,一個是sql中的參數(shù)。第三個是需要傳一個接口RowMapper,這個接口具體是干啥的上網(wǎng)一查就知道了~~~
這里面有一個方法:
T mapRow(ResultSet rs, int rowNum) throws SQLException 復(fù)制代碼第一個參數(shù)是查詢的結(jié)果,第二個是指現(xiàn)在在第幾行結(jié)果,返回值是你要返回什么對象。這里我們需要重寫這個方法,把查詢出的結(jié)果轉(zhuǎn)換成為我們需要的對象。我們可以這么寫:
/*** 把數(shù)據(jù)庫查詢的結(jié)果與對象進(jìn)行轉(zhuǎn)換** @param resultSet* @param rowNum* @return* @throws SQLException*/ public T mapRow(ResultSet resultSet, int rowNum) throws SQLException {Map<String, Object> resultMap = columnMapRowMapper.mapRow(resultSet, rowNum);。。。。 復(fù)制代碼這個方法中的columnMapRowMapper 就是上面我們寫的PlusColumnMapRowMapper,它的作用就是將查詢結(jié)果第 rowNum 拿出來,并且將結(jié)果轉(zhuǎn)換過成為一個 Map<String, Object>。其中:
key :是表字段名稱。
Object :該字段的值。
上面寫的PlusColumnMapRowMapper主要作用就是在獲取值的時候如果發(fā)生異常,返回一個null。
在這一步里我們已經(jīng)拿到了執(zhí)行sql的結(jié)果,現(xiàn)在我們要將結(jié)果轉(zhuǎn)換過為我們需要的class。
將結(jié)果轉(zhuǎn)換為class
在上一步我們拿到了存放結(jié)果Map,現(xiàn)在只需要將map遍歷一下,然后實例化java對象,根據(jù)字段和屬性的映射關(guān)系使用反射將屬性一個個的set進(jìn)去就好了。現(xiàn)在貼上上一步的完整代碼:
public T mapRow(ResultSet resultSet, int rowNum) throws SQLException {Map<String, Object> resultMap = columnMapRowMapper.mapRow(resultSet, rowNum);T instance = getInstance(tableClass);for (Map.Entry<String, Object> entry : resultMap.entrySet()) {//數(shù)據(jù)庫字段名String key = entry.getKey();if (!columnFieldMapper.containsKey(key)) {continue;}Field declaredField = columnFieldMapper.get(key);if (declaredField == null) {continue;}//數(shù)據(jù)庫字段值Object value = entry.getValue();setFieldValue(instance, declaredField, value);}return instance; } 復(fù)制代碼其中 columnFieldMapper 是一個Map<String, Field>。key是表的字段個名稱。value是對應(yīng)的class的屬性。
下面是 setFieldValue的具體代碼:
boolean setFieldValue(T t, Field field, Object value) {field.setAccessible(true);try {if (value != null) {field.set(t, value);return true;}} catch (IllegalAccessException e) {e.printStackTrace();}return false; } 復(fù)制代碼這樣,就可以將查詢出的結(jié)果根據(jù)映射關(guān)系轉(zhuǎn)換成為我們需要的class了。
其他的
如果查詢需要添加條件的話,可以使用之前講的 生成條件的工具將條件的sql拼接在這里的sql后面,相應(yīng)的,where里的參數(shù)也要按照順序添加進(jìn)數(shù)組就好了。
相同的,如果要添加 ORDER BY/GROUP BY/LIMIT這些東西的話也是一樣的操作。主要還是要看自己的代碼是怎么設(shè)計的了。我自己用的只寫了ORDER BY 和 LIMIT 。可以在我的github上找到。地址在這里:github.com/hjx60149632… 。
?
生成sql:delete
思路
誒呀, 這個太簡單了。不寫了哦~~~
參照我之前寫的,分析一下,想一想思路,然后每一步要怎么做,一點一點的就寫好了。
~~~
實現(xiàn)
你自己寫咯~~~。
生成sql:update
最后一部分了,馬上就寫完了。寫東西真的好累啊~~~
思路
更新的語句也比較好做,sql后面的條件因為在之前已經(jīng)寫了where這一篇,所以這里就只寫sql中where左邊的一部分。現(xiàn)在還是先分析一下 **update **語句:
UPDATE user SET name = ? , id = ? , create_date = ? , age = ? , status = ? WHERE id = ? 復(fù)制代碼可以看到的,大體上就是 UPDATE 表名稱 SET 字段名稱 = ? 這個樣子的。(因為現(xiàn)在不寫WHERE右邊的)
所以具體的思路就是:
根據(jù)映射關(guān)系拼裝sql。
這里可能有一個可以選擇的地方,就是如果某一個屬性的值是null,這時要不要把這個屬性更新為null。
拿到要更新的值。
執(zhí)行sql。
實現(xiàn)
從映射中拿到所有的屬性。
這一步的代碼就不放了~~~,和前面寫的沒有什么區(qū)別。
拿到要更新的屬性名稱,和值。
這里我們需要三個參數(shù):
1:用來標(biāo)示更新的時候是否需要忽略值是null的屬性。 boolean ignoreNull
2:用來保存需要更新的字段的有序集合。 List updataColumn
3:保存需要更新的字段的值的有序集合。 List values
代碼是這樣的:
List<String> columnNames = new ArrayList<>(entityTableRowMapper.getColumnNames()); Map<String, Field> columnFieldMapper = entityTableRowMapper.getColumnFieldMapper(); List<Object> values = new ArrayList<>(); for (int i = 0; i < columnNames.size(); i++) {String columnName = columnNames.get(i);if (!sqlColumns.contains(columnName)) {continue;}Field field = columnFieldMapper.get(columnName);Object value = EntityUtils.getValue(entity, field);//如果class中的值是null,并且設(shè)置忽略null,跳過if (ignoreNull && value == null) {continue;}updataColumn.add(columnName);values.add(value); } 復(fù)制代碼根據(jù)拿到的數(shù)據(jù)拼裝sql
拿到上面需要的數(shù)據(jù)后,我們還需要拿到表的名稱,這一步直接從映射關(guān)系中取就好了。下面的是拼裝sql的代碼:
StringBuilder sql = new StringBuilder(); sql.append("UPDATE ").append(getTableName()).append(StringUtils.SPACE); sql.append("SET "); for (int i = 0; i < updataColumn.size(); i++) {String column = updataColumn.get(i);if (i == 0) {sql.append(StringUtils.append(column, " = ? "));} else {sql.append(StringUtils.append(", ", column, " = ? "));} } 復(fù)制代碼這樣就好了,大致上是這樣的:
UPDATE user SET name = ? , id = ? , create_date = ? , age = ? , status = ? 復(fù)制代碼條件的話,用之前寫的where生成就好了,where中的值加在集合values的后面就好了。
執(zhí)行sql。
太簡單了,就不寫了~
最后
終于寫完了。
還是說一下,因為代碼已經(jīng)在github上了,所以沒有把全部的代碼寫在上面,主要還是以說明思路為主。另外剛開始寫博客,有些可能表達(dá)的不是很明白。吃了沒文化的虧啊~~~
這個項目還有很多可以但是還沒有實現(xiàn)的功能,比如一些比較復(fù)雜的查詢,執(zhí)行函數(shù)之類的。我并沒去寫它。一是不需要,因為這個東西平時主要是做導(dǎo)出報表的時候用的,二是我自己寫項目的話壓根就不會用到這些東西,能用java寫的我都用java寫了。數(shù)據(jù)庫嘛,對我來說就存?zhèn)€數(shù)據(jù)就好了,數(shù)據(jù)處理上的事情還是交給java來做好一點。
完了
?
轉(zhuǎn)載于:https://juejin.im/post/5c73a7d16fb9a049fc042938
總結(jié)
以上是生活随笔為你收集整理的手把手教你写一个Java的orm框架(4)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开工的欲望 | AI Studio上线新
- 下一篇: Java的新项目学成在线笔记-day6(