2021SC@SDUSC
AbstractSQLExecutor—SQL执行器
首先看一下抽象SQLExecutor的主要核心代码,此方法的作用就是去执行SQL语言
准确来说,我们是会在其它的类里面去通过赋值某些参数从而调用sqlconfig对象去执行,此方法首先就是传入SQLConfig类型的一个参数,以便于直到SQL执行的SQL语言配置,然后就是获取到传入参数的各种SQL配置,并验证sql语言的合法性。
然后就是开始确切执行sql语言,开始与数据库进行交互以获得返回数据。
这个方法由于比较关键,所以也就比较繁多,我们一起来看一下:
public JSONObject execute(@NotNull SQLConfig config, boolean unknowType) throws Exception {
boolean isPrepared = config.isPrepared();
final String sql = config.getSQL(false);
config.setPrepared(isPrepared);
if (StringUtil.isEmpty(sql, true)) {
Log.e(TAG, "execute StringUtil.isEmpty(sql, true) >> return null;");
return null;
}
boolean isExplain = config.isExplain();
boolean isHead = RequestMethod.isHeadMethod(config.getMethod(), true);
final int position = config.getPosition();
JSONObject result;
if (isExplain == false) {
generatedSQLCount ++;
}
ResultSet rs = null;
List<JSONObject> resultList = null;
Map<String, JSONObject> childMap = null;
try {
if (unknowType) {
Statement statement = getStatement(config);
rs = execute(statement, sql);
result = new JSONObject(true);
int updateCount = statement.getUpdateCount();
result.put(JSONResponse.KEY_COUNT, updateCount);
result.put("update", updateCount >= 0);
//导致后面 rs.getMetaData() 报错 Operation not allowed after ResultSet closed result.put("moreResults", statement.getMoreResults());
}
else {
switch (config.getMethod()) {
case POST:
case PUT:
case DELETE:
executedSQLCount ++;
int updateCount = executeUpdate(config);
if (updateCount <= 0) {
throw new IllegalAccessException("没权限访问或对象不存在!"); // NotExistException 会被 catch 转为成功状态
}
// updateCount>0时收集结果。例如更新操作成功时,返回count(affected rows)、id字段
result = AbstractParser.newSuccessResult(); // TODO 对 APIAuto 及其它现有的前端/客户端影响比较大,暂时还是返回 code 和 msg,5.0 再移除 new JSONObject(true);
//id,id{}至少一个会有,一定会返回,不用抛异常来阻止关联写操作时前面错误导致后面无条件执行!
result.put(JSONResponse.KEY_COUNT, updateCount);//返回修改的记录数
if (config.getId() != null) {
result.put(config.getIdKey(), config.getId());
} else {
result.put(config.getIdKey() + "[]", config.getWhere(config.getIdKey() + "{}", true));
}
return result;
case GET:
case GETS:
case HEAD:
case HEADS:
result = isHead ? null : getCacheItem(sql, position, config.getCache());
Log.i(TAG, ">>> execute result = getCache('" + sql + "', " + position + ") = " + result);
if (result != null) {
cachedSQLCount ++;
Log.d(TAG, "\n\n execute result != null >> return result;" + "\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n");
return result;
}
rs = executeQuery(config); //FIXME SQL Server 是一次返回两个结果集,包括查询结果和执行计划,需要 moreResults
if (isExplain == false) { //只有 SELECT 才能 EXPLAIN
executedSQLCount ++;
}
break;
default://OPTIONS, TRACE等
Log.e(TAG, "execute sql = " + sql + " ; method = " + config.getMethod() + " >> return null;");
return null;
}
}
if (isExplain == false && isHead) {
if (rs.next() == false) {
return AbstractParser.newErrorResult(new SQLException("数据库错误, rs.next() 失败!"));
}
result = AbstractParser.newSuccessResult();
result.put(JSONResponse.KEY_COUNT, rs.getLong(1));
resultList = new ArrayList<>(1);
resultList.add(result);
}
else {
resultList = new ArrayList<>(config.getCount() <= 0 ? Parser.MAX_QUERY_COUNT : config.getCount());
// Log.d(TAG, "select cache = " + cache + "; resultList" + (resultList == null ? "=" : "!=") + "null");
我们看到首先是定义了一个变量,然后用config对象的isPrepared的方法返回了一个布尔值,这个其实是是否预编译,这个相信大家也都清楚,然后就是获取到相应的sql并且对获取到的sql进行一个空值的判断,然后就是判断了是否为头方法,头方法使我们定义的一种请求类型,既然碰到了。我们就来了解一下HEAD请求类型,其实和我们的GET其实基本一致
HEAD
HEAD方法和GET方法一致,除了服务器不能在响应里返回消息主体。HEAD请求响应里HTTP头域里的元信息应该和GET请求响应里的元信息一致。此方法被用来获取请求实体的元信息而不需要传输实体主体(entity-body)。此方法经常被用来测试超文本链接的有效性,可访问性,和最近的改变。.
HEAD请求的响应是可缓存的,因为响应里的信息可能被用于更新以前的那个资源的缓存实体.。如果出现一个新的域值指明了缓存实体和当前源服务器上实体的不同(可能因为Content-Length,Content-MD5,ETag或Last-Modified值的改变),那么缓存(cache)必须认为此缓存项是过时的(stale)。
那么然后就是对请求类型的判断,当GET或者POST请求时便缺省,当请求为DELETE时,便判断当前有无权限,如果有权限便将执行的记录数量返回,这里可以看到就是熟悉的创建Statement对象,然后用这个对象去执行executeUpdate()等直接与数据库交互的操作,也就是我们熟知的JDBC,然后就是当类型方法为HEADS时rs = executeQuery(config),此时的 SQL Server 是一次返回两个结果集,包括查询结果和执行计划,需要 处理一个更多的结果集。
最后这里是设置了初始容量为查到的数据量,是为了解决频繁扩容导致的延迟,不够这个貌似只有 rs.last 取 rs.getRow() 然后又得 rs.beforeFirst 重置位置以便下方取值
然后就就是对具体sql的处理以及数据的更新:
//<SELECT * FROM Comment WHERE momentId = '470', { id: 1, content: "csdgs" }>
// WHERE id = ? AND ... 或 WHERE ... AND id = ? 强制排序 remove 再 put,最好还是重新 getSQL
int index = -1;
ResultSetMetaData rsmd = rs.getMetaData();
final int length = rsmd.getColumnCount();
childMap = new HashMap<>(); //要存到cacheMap
这个使我们以后要存放关联表查询的缓存,也就是我们在关联表的查询得到的数据,就会放入到这个childMap的缓存里面。
boolean hasJoin = config.hasJoin();
int viceColumnStart = length + 1; //第一个副表字段的index
while (rs.next()) {
index ++;
JSONObject item = new JSONObject(true);
for (int i = 1; i <= length; i++) {
// bugfix-修复非常规数据库字段,获取表名失败导致输出异常
if (isExplain == false && hasJoin && viceColumnStart > length) {
List<String> column = config.getColumn();
String sqlTable = rsmd.getTableName(i);
if (config.isClickHouse()&&(sqlTable.startsWith("`")||sqlTable.startsWith("\""))){
sqlTable = sqlTable.substring(1,sqlTable.length()-1);
}
if (column != null && column.isEmpty() == false) {
viceColumnStart = column.size() + 1;
}
else if (config.getSQLTable().equalsIgnoreCase(sqlTable) == false) {
viceColumnStart = i;
}
}
item = onPutColumn(config, rs, rsmd, index, item, i, isExplain == false && hasJoin && i >= viceColumnStart ? childMap : null);
}
resultList = onPutTable(config, rs, rsmd, resultList, index, item);
}
}
}
finally {
if (rs != null) {
try {
rs.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
if (resultList == null) {
return null;
}
if (unknowType || isExplain) {
if (isExplain) {
if (result == null) {
result = new JSONObject(true);
}
config.setExplain(false);
result.put("sql", config.getSQL(false));
config.setExplain(isExplain);
config.setPrepared(isPrepared);
}
result.put("list", resultList);
return result;
}
if (isHead == false) {
// @ APP JOIN 查询副表并缓存到 childMap <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
executeAppJoin(config, resultList, childMap);
//子查询 SELECT Moment.*, Comment.id 中的 Comment 内字段
Set<Entry<String, JSONObject>> set = childMap.entrySet();
for (Entry<String, JSONObject> entry : set) {
List<JSONObject> l = new ArrayList<>();
l.add(entry.getValue());
putCache(entry.getKey(), l, JSONRequest.CACHE_ROM);
}
putCache(sql, resultList, config.getCache());
Log.i(TAG, ">>> execute putCache('" + sql + "', resultList); resultList.size() = " + resultList.size());
// 数组主表对象额外一次返回全部,方便 Parser 缓存来提高性能
result = position >= resultList.size() ? new JSONObject() : resultList.get(position);
if (position == 0 && resultList.size() > 1 && result != null && result.isEmpty() == false) {
// 不是 main 不会直接执行,count=1 返回的不会超过 1 && config.isMain() && config.getCount() != 1
result = new JSONObject(result);
result.put(KEY_RAW_LIST, resultList);
}
}
return result;
}
这里面有一个hasJoin的变量,这个变量也是一个重要的点,就是说查询的时候是否会关联其它的表,这个的取值是会影响最后的结果的。
然后就是对数据库访问后的结果集的处理,先是对获取表名失败处理了一下异常,然后就是正常的获取结果并且存放到我们要返回的数据结构。 之后就是当不是head方法的时候,先是执行了一下join,然后新建了一个子查询,将子查询查到的数据放入到了Cache里面,也就是我们预先设置的childMap里面,回来需要的时候,便从这里面取。
在其中updateCount值由下面方法获取,也就是一个简单的数据更新,
public int executeUpdate(@NotNull SQLConfig config) throws Exception {
PreparedStatement s = getStatement(config);
int count = s.executeUpdate(); //PreparedStatement 不用传 SQL
if (config.getMethod() == RequestMethod.POST && config.getId() == null) { //自增id
ResultSet rs = s.getGeneratedKeys();
if (rs != null && rs.next()) {
config.setId(rs.getLong(1));//返回插入的主键id
}
}
return count;
}
同时就是这个方法,当得到POST请求时如果GETId为null则获取到结果集的ID,然后设置为此Config的id。
本次与小组成员讨论
这次是我刚开始不太明白HEAD请求的不同之处,然后和组员进行了问询与讨论,我们查阅了相关的资料,我也在网上找了很多相关的解释和博客,最后发现,其实把它想复杂了,HEAD和GET的原理和机制基本是一样的,但是两者的作用就是最大的差别,GET: 请求指定的页面信息,并返回实体主体。 HEAD: 只请求页面的首部。,这也给了我一个警示,有时候问题可能就在表面,不要操之过急,总会水落石出,同时,在代码的分析过程中,大量的查资料是不可避免的,也正是能有大量的查了资料的机会我们才能通过分析代码去收获更多的知识,这些知识一般无可琢磨,在这种实际的开发项目中就会很容易发现。