浅浅地封装一层 JDBC

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: 写在数据库重构前面的话——说说我之前认识的数据库 数据库向来是个“老大难”问题。好的数据库设计与好的数据库程序密切相关,一个逻辑清楚、管理维护方便的程序往往离不开优秀的数据库表设计。本人之前就是基础不牢,对这个问题认识不够,忽视了数据库表设计,导致了后来程序层(ASP/JSP)怎么写都不顺当——好生烦恼!究其原因,就是把关系型的数据库的问题简单化、想当然。

写在数据库重构前面的话——说说我之前认识的数据库

数据库向来是个“老大难”问题。好的数据库设计与好的数据库程序密切相关,一个逻辑清楚、管理维护方便的程序往往离不开优秀的数据库表设计。本人之前就是基础不牢,对这个问题认识不够,忽视了数据库表设计,导致了后来程序层(ASP/JSP)怎么写都不顺当——好生烦恼!究其原因,就是把关系型的数据库的问题简单化、想当然。如今是该正视问题的时候了!

首先我最开始接触的是 FoxBase,那时是在学校的数据库课程上,还有学校日常的考勤工作,我也有幸参与了,都是用 FoxBase/FoxPro 进行管理的。现在语法已经忘光了,感觉那时候学得容易,也直观。学校里也开了一课 Access,但老师讲得敷衍(确实如此,不是我不尊重老师)。

后来自学 ASP,也涉及到 SQL。于是简单学习了一下 SELECT/UPDATE/INSERT INTO 等这样的操作语句。因为是自学,没有经过数据库理论系统的学习。我们知道,数据库尤其是关系型数据库是一门独立的学科,在计算机课程中显得十分重要。这门课除 SQL 操作语句之外,其中相当于知识点都是偏理论和数学的,例如关系代数、范式等。我惧怕这些知识,因此也没有经过扎实的学习,而是参考了一个简单 ASP 新闻发布系统,参考了里面 Access 的设计。不得不说,我可作为一个反面例子,说明这样求学的心态是如何浮躁的。

有一点还记得的,就是学会了 SQL Join 语句之后,好像立刻获得什么重大成就那样子,马上兴奋异常。不过确实感觉到了 Join 的强大,可以做以前做不了的很多事情——窃以为,这就是“自学”的好处吧,容易获得成就感,哪怕是一点一滴的,且自我感觉良好。

后学我花时间在语言方面比较多,也没怎么关注数据库。倒是接触 Java/Hibernate 之际了解了什么是 ORM,知道啥是“阻抗不匹配”。

在了 ROR 框架的时候,发现其所使用 ActiveRecord 不错,感觉理解起来挺容易。于是尝试自己写  ActiveRecord 的实现,还是用 ASP/Jscript 的。后来归纳总结,所谓 ActiveRecord 属于数据库模式中的一种,所谓 belongsTo、hasMany 无非就是原来的“一对多”、“多对一”之类的,没有跳出数据库的“五指山”——而那时的我盲目跟风,只知其一不知其二。

再后来业界“NoSQL”的蹿红,使得我更有理由“鄙视”关系型数据库,觉得反正用不用 MySQL、SQL Server 都可以——唉,无知呐。

ORM 会给人一种错觉:使人觉得数据库的问题可以在语言层面得到解决。我为此开始了“孜孜不倦”尝试,从 ASP 到 JSP,总以为可以封装好 SQL。然而,我却不是赞成使用 Hibernate 这些框架,甚至轻视!感觉其中无非就是 SQL 语句的拼凑——要不我自己来吧!我甚至到了一种地步,把面向对象的“继承”生搬硬套到表设计中!那,当然是要“闹笑话”的——唉!

“表设计很重要”,这是我一个写后台的同事说的。我的表那么多冗余、设计的那么不自然,——为什么我还能不以为然、还能容忍这种情况存在?难道 Java 层面就可以把 SQL 完全搞定??

总之,我一再反思,希望找出问题的根源。

目标

请注意:该文章已过时,请留意最新版本,相关资源在下一节《Bigfoot 数据库 CRUD 举例 》

众所周知,通过 JDBC 我们可以向数据库询问和写入数据(增、删、改、查)。但直接使用 JDBC 开发效率不高,于是我们想想怎么封装一层吧!

大目标:轻量级,使数据库操作更简单、更顺手、更纯粹!

目标有大必有小。小的具体目标如下:

  • 使用 Tomcat 自带的数据库连接池 JDBC Pool。
  • 提倡先写 SQL,看是否 OK,再封装。实际上,无论什么业务,都是产生 SQL 然后交给 JDBC 执行的,所以 SQL 是最终的“编译”结果。
  • 封装是什么封装方法?这是一种半自动化 ORM,既非手工的 SQL 字符串拼凑,而是提供一种简单的方法维护 SQL。不用写直接写 SQL,而是 Java + SQL。
  • 即使封装 SQL 语句,也不要过渡封装。
  • 采用 Map 为记录载体
  • 与 JS 引擎 Rhino 无缝对接,用 JS 定义一些方法
  • 抛弃 Java POJO,以 JS 定义实体,适合前后台所使用

缺点是:一、通用性不会很强,只限于当前框架所使用,约定的关系较多,耦合紧;二、当前只针对 MySQL/SQLite。

基础模块

之所以有这个基础模块,其实是排除了 DAO(Data Access Object) 所剩余的 SQL 包下面的内容。关于 DAO 模块,后面会论述。

Base

Base 围绕下面几个常见的问题而设,也就是把一些共性的特征给抽象出来,是底层的类。Base 是一个抽象类(Abstract Class),虽然他们之间没有必然的关联关系,但还是形成了这个类,属于“自底向上”的设计。使用这个抽象类有下面介绍的 Reader、Writer。

具体流程参见最新源码:http://code.taobao.org/p/bigfoot_v2/src/java/com/ajaxjs/framework/sql/Base.java

一言蔽之,Connection 对象,每个数据库首先都得创建这个对象——所以是必须的;TableName,不言而喻,怎么能不知道表名?;id 是指列名具体是什么?id 还是 uid,要说清楚;最后的资源释放操作,是关于 Connection、ResultSet 和 Statement 的。于是围绕上述几点,有下面的方法。

这些字段均是 getter/setter。Conn 就特殊点,因为初始化数据库连接对象提供了若干重载方法,适应不同场合。针对 Conn 还有释放资源的方法。

Result

具体流程参见最新源码:http://code.taobao.org/p/bigfoot_v2/src/java/com/ajaxjs/framework/sql/Result.java

数据,可定义为有意义的实体,它涉及事物的存在形式——通常不止一种形式,在数据库里是一种形式,在 Java 程序里是一种形式,在接口层面又是另外一种形式。针对查询操作,应该有专门的类保存数据的不同形式,有 JSON、Map 和 Map[],还有记录操作过的 SQL 语句,保存起来在日志文件中。Result 对象没有使用 getter/setter,而是直接使用对象的属性。结构如下。

/**
 * 查询结果
 * @author Frank Cheung
 */
public class Result {
	/**
	 * 返回前端的 JSON 字符串
	 */
	public String json = null;
	
	/**
	 * 结果集合,复数,如果单数则是 jsArr.get(0)
	 */
	public NativeArray jsArr = null;
	
	/**
	 * 执行过查询的那个 SQL 语句。可用于追溯
	 */
	public String sqlStatement = null;
	
	/**
	 * 已转换为 Map 的结果,如果复数则是 null
	 */
	public Map<String, Object> result  = null;
	
	/**
	 * 已转换为 Map 结果集合,如果单数则是 null
	 */
	public Map<String, Object>[] results = null;
	
	/**
	 * 查询耗时
	 */
	public Date queryTime = null;
}

该类的设计充分符合了为方便 JSON 接口输出的原则(故内部也是用 Rhino 遍历 ResutSet,直接返回 JSON)。接口每次请求完毕,都会读取 Result.json 的结果。如果要在 Java 中作业务逻辑处理,则读取 Result.result/results 即可。

SQL

这是对 SQL 字符串语句的封装。我曾经为这个模块想到过的名字“SQL 编译器”,虽然有点贴切,但未免口气太大,与实际情况有点不符,想想这个名字还是算了。我的目标是这样的:应该是一种“半自动化 ORM”,既非手工的 SQL 字符串拼凑,而是提供一种简单的方法维护 SQL。不用写直接写 SQL,而是 Java + SQL。不是想代替 SQL,SQL 的作用仍非常重要的,此处只是用一种简单的方法封装 SQL 操作,不用直接写 SQL 那么辛苦。

这个类的作用有点类似于 QueryRunner

实际成果究竟怎么样?不妨先看看调用的例子,感觉下如何。

例子如下(摘自单元测试)。

String sqlStr;
Sql sql = new Sql();
sql.setTableName("User");
sql.setSql("SELECT * FROM " + sql.getTableName());
sqlStr = sql.query();
assertEquals("SELECT * FROM User", sqlStr);
assertEquals(sqlStr, sql.query()); // 没有其他条件,此时 应是一致的,

// 排序
sql.setOrderBy(String.format(" ORDER BY %s.id DESC", sql.getTableName()));
sqlStr = sql.query();
assertEquals("SELECT * FROM User ORDER BY User.id DESC", sqlStr);

// 需要分页的
sql.isPage  = true;
sql.setLimit(3, 9);
sqlStr = sql.query();
assertEquals("SELECT * FROM User ORDER BY User.id DESC LIMIT 3, 9", sqlStr);

// WHERE 条件查询
sql.addWhere("id = 1000");
sql.addWhere("isAdmin = true");
sqlStr = sql.query();		
assertEquals("SELECT * FROM User WHERE (id = 1000 AND isAdmin = true) ORDER BY User.id DESC LIMIT 3, 9", sqlStr);

sql.addWhere_OR("name = 'Jack'");
sql.addWhere_OR("name = 'Mike'");
sqlStr = sql.query();
assertEquals("SELECT * FROM User WHERE (id = 1000 AND isAdmin = true) AND (name = 'Jack' OR name = 'Mike') ORDER BY User.id DESC LIMIT 3, 9", sqlStr);

// 求结果总数
sqlStr = sql.findRowCountSql();
assertEquals("SELECT COUNT(User.uid) AS count FROM User WHERE (id = 1000 AND isAdmin = true) AND (name = 'Jack' OR name = 'Mike')", sqlStr);
System.out.println(sqlStr);

如例子所示,SQL 类做的事情很简单。而且我规定 SQL 类只是返回 String 类型的 SQL 语句,皆是字符串的操作和输入、输出,不涉及的数据库执行等的其他问题,目的是令其专注。

当前封装了 SQL 语句以下查询的功能:

  • WHERE
  • LIMIT
  • ORDER BY
  • Query Total AS Int

此外,还有写操作的部分有待完善。

Data Access Object

DAO 实际上是一个接口,详情如下:

import java.sql.Connection;
import com.ajaxjs.sql.Result;
import com.ajaxjs.sql.Sql;

public interface DAO {
    public Connection getConn();
    public void setConn(Connection conn);
    public Result query(String sql, boolean isSolo);
    public int queryAsInt(String sql);
    public Result findById(Sql sqlInstance, String id);
    public Result findList(Sql sqlInstance, Integer start, Integer limit);
    public int executeUpdate(String sql);
    public int create(Map<String, Object> pair);
    public int update(Map<String, Object> pair, String id);
    public boolean delete(String uid);
}

DAO 的目的是为其他层提供访问数据库的服务。程序员无须知道其内部细节,就可以轻松调用数据库。

具体流程参见最新源码:http://code.taobao.org/p/bigfoot_v2/src/java/com/ajaxjs/framework/sql/dao/DAO.java

然而 DAO 内部是复杂的,代码层面是被划分为多个类的,有 Base、Reader、Writer。Base 就是前文提到过的抽象类。它们之间是逐层继承的。最终由 Writer 实现了 DAO 接口。从 DAO 往 Writer、Reader 看,是“自顶向下设计(from Up to Button design)”,也就是了实现了一大块的功能块,然后抽出哪些需要的形成一个接口,这样的设计比较灵活,显示了多态的威力,也比较吻合我的初衷。

值得一提的是,Reader 和 Writer 可以分别单独使用。

Reader

Reader 为只读的数据库访问类。

query()

public Result query(String sql, boolean isSolo); 是通用方法,参数是 SELECT 语句,isSolo 是 boolean 参数,说明返回的结果是否单数。单行记录的是单数的意思,多数的意思就是多行记录。返回的结果保持 Result.result(单数)/ Result.results(多数)中。

例子如下(摘自单元测试)。

String sql = "SELECT * FROM news";

Reader reader = new Reader(conn); // conn 是数据库连接对象
Map<String, Object>[] list = reader.query(sql, false).results;
assertNotNull(list);
assertNotNull("总数:", list.length);

int total = reader.queryAsInt("SELECT COUNT(name) AS total FROM " + tableName);
assertNotNull("总数:", total);

query() 方法内部就是封装了一些异常和是否有结果的判断,还有就是 Statement、ResultSet 对象的资源释放,并进行适当的类型转换。

findById()

public Result findById(Sql sql, String id) 对 query() 简单封装,SQL 语句的变化就是在后面加上 WHERE id = xxx,这是单数的查询。要求传入 Sql 对象 和 id 参数。

WHERE id = xxx 中 id 列名是允许指定的。

例子如下(摘自单元测试)。

Sql sql = new Sql();
sql.setTableName(tableName);
Reader reader = new Reader(conn);
Result result = reader.findById(sql, "49d09367-8678-4ecb-8921-d24320e11f96");

assertNotNull(result.result);
assertEquals(result.result.get("name"), "fooo");

System.out.println(result.json);

findList()

public Result findList(Sql sql, Integer start, Integer limit) 读取列表(设置排序、是否需要分页)。如果不需要分页传入第二、第三个参数 null 即可。

例子如下(摘自单元测试)。

Sql sql = new Sql();
sql.setTableName(tableName);
Reader reader = new Reader(conn);
Result result = reader.findList(sql, null, null);

assertNotNull(result.results);
//		assertEquals(result.results[0].get("name"), "fooo");

System.out.println(result.json);

findlList() 结合 Sql 进行操作。如果纯粹 SQL 字符串输入,建议使用 query() 方法(没有分页特性,自己处理)。

Writer

Writer 为写入的数据库访问类,大体可分为 create 和 update 两项操作。

执行 SQL 语句

简单的执行 create/update 操作,没什么好说的。

例子如下(摘自单元测试)。

Writer writer = new Writer(conn);

int newlyId = writer.create("INSERT INTO news (name) VALUES ('hihi')");
String msg = "插入一条新纪录(pure sql)";
System.out.println(msg + newlyId);

assertNotNull(msg, newlyId);

newlyId 是影响的行数或者新建记录的 id。

输入 Map/String[] pair/String[] columns、values

这时候必须输入要操作的表名。

例子如下(摘自单元测试)。

Writer writer = new Writer(conn);
writer.setTableName("news"); // 必须设置表名

Map<String, Object> pair = new HashMap<String, Object>();
pair.put("uid",  java.util.UUID.randomUUID().toString());
pair.put("name", "fooo");

String msg = "插入一条新纪录(Map 载体)";
int newlyId = writer.create(pair);
System.out.println(msg + newlyId);

assertNotNull(msg, newlyId);

也可以不同形式的 String[] pair/String[] columns、values。

例子如下(摘自单元测试)。

@Test
public void createByArray(){
	Writer writer = new Writer(conn);
	writer.setTableName(ReaderTest.tableName);
	
	String[] pair = new String[]{
		"name=bar", "content=my content"
	};
	
	String msg = "插入一条新纪录(String[] pair)";
	int newlyId = writer.create(pair);
	System.out.println(msg + newlyId);
	
	assertNotNull(msg, newlyId);
}

@Test
public void createByArrayCol_Val(){
	Writer writer = new Writer(conn);
	writer.setTableName(ReaderTest.tableName);
	
	String[] columns = new String[]{
		"name", "content"
	}, values = new String[]{
		"foo", "your content"
	};
	
	String msg = "插入一条新纪录(String[] columns, values)";
	int newlyId = writer.create(columns, values);
	System.out.println(msg + newlyId);
	
	assertNotNull(msg, newlyId);
}

Writer 满足了以下需求:

  • 传入 Map,生产 Update/INSERT 语句
  • 传入 Request,生成 Update/INSERT 语句
  • 普通 SQL 语句,执行 JDBC executeUpdate()
  • INSERT 返回新增 id

设计 Writer 的时候考虑了一下几点,一、Statement 和 PreparedStatement 写法太不一样,要兼容的话太麻烦。干脆直接使用 PreparedStatement。仅保留 Statment 用于简单的 SQL 执行;二、有不同类型、不同结构的参数传入,种类繁复,最后通通转换为 Map<String, Object> 处理,不管传入的是什么类型的数据,数组还是 Map<String, String>[]。

例子如下(摘自单元测试)。

@Test
public void updateByMap(){
	Writer writer = new Writer(conn);
	writer.setTableName(ReaderTest.tableName);
	
	Map<String, Object> pair = new HashMap<String, Object>();
	pair.put("uid",  java.util.UUID.randomUUID().toString());
	pair.put("name", "fooo");
	
	String msg = "更新一条新纪录(Map 载体)";
	int newlyId = writer.update(pair, "49d09367-8678-4ecb-8921-d24320e11f96");
	System.out.println(msg + newlyId);
	
	assertNotNull(msg, newlyId);
}

@Test
public void updateByArray(){
	Writer writer = new Writer(conn);
	writer.setTableName(ReaderTest.tableName);
	
	String[] pair = new String[]{
		"name=bar", "content=my content"
	};
	
	String msg = "插入一条新纪录(String[] pair)";
	int newlyId = writer.update(pair, "49d09367-8678-4ecb-8921-d24320e11f96");
	System.out.println(msg + newlyId);
	
	assertNotNull(msg, newlyId);
}

@Test
public void updateByArrayCol_Val(){
	Writer writer = new Writer(conn);
	writer.setTableName(ReaderTest.tableName);
	
	String[] columns = new String[]{
		"name", "content"
	}, values = new String[]{
		"foo", "your content"
	};
	
	String msg = "插入一条新纪录(String[] columns, values)";
	int newlyId = writer.update(columns, values, "e83b3561-dcd9-45b6-a8a9-bc149c35f3eb");
	System.out.println(msg + newlyId);
	
	assertNotNull(msg, newlyId);
}

删除记录

删除就显得简单多了。

Writer writer = new Writer(conn);
writer.setTableName(ReaderTest.tableName);

String uuid = java.util.UUID.randomUUID().toString();
Map<String, Object> pair = new HashMap<String, Object>();
pair.put("uid",  uuid);
pair.put("name", "fooo");

String msg = "插入一条新纪录(Map 载体)";
int newlyId = writer.create(pair);
assertNotNull(msg, newlyId);

assertEquals(true, writer.delete(uuid));

至此,SQL 这一大模块的介绍,就到此暂告一段落了。

再计一个数据库工具类(以废弃)

源码:

https://code.google.com/p/naturaljs/source/browse/trunk/java/src/ajaxjs/sql/DAO.java

一、数据库连接

先说说构造器。

/**
 * 连接数据库。
 * @param jdbcConn 数据库连接对象
 */
public DAO(Connection jdbcConn){
	this.jdbcConn = jdbcConn;
	checkIfNull();
}

/**
 * 连接数据库(使用连接池)
 * @param path 连接池的路径
 */
public DAO(String path){
	this.jdbcConn = getConn(path);
	checkIfNull();
}
开发者可以直接传入一个 Connection 类型的对象,或者 String。String 代表是什么呢?是连接池的路径。本框架通过 TOMCAT 7 连接池获取数据库连接,不需要其他第三方的 jar,使用起来比较方便、简单。于是 Strting path 是 META-INF 目录下 Content.xml 配置文件的连接路径,可以是 SQLite、MySQL 任意的 Tomcat 7 支持的连接池。详见 getConn(String path) 方法。
/**
 * 创建数据库连接对象(通过 TOMCAT 7 连接池 获取数据库连接)
 * @param path Context.xml 配置文件中路径
 * @return
 */
public static Connection getConn(String path){
	Connection conn = null;
	
	try{
		javax.naming.Context envContext  = (javax.naming.Context)new javax.naming.InitialContext().lookup("java:/comp/env"); 
		javax.sql.DataSource ds = (javax.sql.DataSource)envContext.lookup(path); 
//			 简写方式
//			javax.sql.DataSource ds = (javax.sql.DataSource)new javax.naming.InitialContext().lookup("java:/comp/env/jdbc/derby"); 
		
		conn = ds.getConnection(); 
		
		System.out.println("数据库连接成功:" + conn);
	}catch(SQLException e){
		System.out.println("数据库连接失败!"); 
		e.printStackTrace();
	}catch (javax.naming.NamingException e) {
		System.out.println("读取数据源的配置文件失败,请检查 Tomcat 连接池配置"); 
		e.printStackTrace();
	}
	
	return conn;
}
当然,框架中还保留传统的 JDBC 连接方式,如 jdbc:mysql://linux-db-bjzwNew2.xincache.cn:3306/net25923067?user=net22963167&password=2crFxx0n0x。下面是重载方法。
/**
 * 创建数据库连接对象(传统方式)
 * @param driver e.g "com.mysql.jdbc.Driver"
 * @param jdbcUrl e.g "jdbc:mysql://linux-db-bjzwNew2.xincache.cn:3306/net25923067?user=net22963167&password=2crFxx0n0x"
 * @param props
 * @return
 */
public static Connection getConn(String driver, String jdbcUrl, java.util.Properties props){
	Connection conn = null;
	
	try {
		Class.forName(driver);
	} catch (ClassNotFoundException e) {
		System.out.println("创建数据库连接失败,请检查是否安装对应的 Driver"); 
		e.printStackTrace();
	}
	
	try {
		if(props == null){
			conn = DriverManager.getConnection(jdbcUrl);
		}else{
			conn = DriverManager.getConnection(jdbcUrl, props);
		}
		System.out.println("数据库连接成功:" + conn);
	} catch (SQLException e) {
		System.out.println("数据库连接失败!"); 
		e.printStackTrace();
	}
	
	return conn;
}
关闭数据库连接也有两个重载的方法。
/**
 * 关闭数据库连接
 */
public void closeConnection(Connection jdbcConn){
	try {
		jdbcConn.close();
	} catch (SQLException e) {
		e.printStackTrace();
	}
}

/**
 * 关闭数据库连接
 */
public void closeConnection(){
	try {
		this.jdbcConn.close();
	} catch (SQLException e) {
		e.printStackTrace();
	}
}

开发者需要手动关闭数据库连接。

查询操作

查询无非返回期待的数据和特定的类型。这里考虑几种使用情况:一、查询 ResultSet;二、任意的查询操作(传入回调);三、查询总记录路数(即 SQL Count() 函数),分别如下方法签名。

/**
 * SQL 的 SELECT 查询,返回 RS 结果集合
 * @param sql
 * @return
 */
public ResultSet query(String sql);

/**
 * 支持回调的查询
 * @param sql
 * @param cb
 * @return
 */
public Object query(String sql, Callback cb);

/**
 * 用于记录总数的返回
 * @param sql
 * @param isCount
 * @return
 */
public int query(String sql, boolean isCount);

它们都调用这个真实的方法。
/**
 * SQL 的 SELECT 查询,既返回 RS 结果集合,也可以返回 SELECT COUNT(id) AS total 的总数。
 * 这是读操作的查询。
 * @param {String} sql SELECT语句
 * @param {Booelan} isCount 是否返回总数
 * @returns {Object} 可变返回值,既可是 RS 结果集,也可以是 int 总数,使用强类型转换获取真实类型。
 */
public Object query(String sql, boolean isCount, Callback cb){
	Object arr = null;
	Statement statement = null;
	ResultSet resultset = null;
			
	System.out.println("将要查询的 SQL 为:" + sql);
	
	try{
		statement = jdbcConn.createStatement();
		resultset = statement.executeQuery(sql);
		
		if(isCount){
			arr = resultset.isBeforeFirst() ? resultset.getInt(1) : null;
			// cover to js number, but it doesn't work here.
//				arr = jsEngine.call("Number", arr);
		}else{
			// 如果没有符合的记录,返回 null 
			if(resultset.isBeforeFirst() && cb != null){
				//arr = jsEngine.call("getValue", resultset);
				cb.doIt(resultset);
			}
		}
		
		if(isCount == false && cb == null){
			// 返回 resultset
			arr = resultset;
		}
		
//			System.out.println("查询成功!:::");
	}catch(Throwable e){
		System.out.println("查询失败!:::" + sql);
		e.printStackTrace();
	}finally{
		if(isCount == false && cb == null){
			// 返回 resultset 不要释放 resultset
			release(statement, null);
		}else release(statement, resultset);
	}
	
	return arr;
}

/**
 * SQL 的 SELECT 查询,返回 RS 结果集合(List类型)
 * @param connection
 * @param sql
 * @return
 */
public List<Map<String, String>> queryAsList(String sql);
其中,回调的方法是强大、灵活的方法。现在很多接口都是 JSON 接口,为此,可以把解析 JSON 的逻辑写进这个 Callback 中。
/**
 * 返回 JSON 格式数据
 * @param sql
 * @param jsEngine
 * @return
 */
public Object queryAsJSON(String sql, final JsRuntime jsEngine){
	this.query(sql, false, new Callback(){
		public Object doIt(ResultSet resultset){
			getJSON_List(jsEngine.call("getValue", resultset));
			return null;
		}
	});
	return JSON_arr;
}	
此处使用了 Rhino JS 引擎 直接序列化的 ResultSet 对象。

写操作

数据库的写操作比较简单,考虑返回的情况主要有以下两种:一、创建 ok,返回新纪录的 id;二、修改 ok,返回影响的行数;这点我已经 SQL 语句的不同作出了识别,返回的是 int。

/**
 * 这是写操作的查询。
 * @param {String} sql
 * @returns {Number} 返回影响的行数
 */
public int executeUpdate(String sql){
	Statement statement = null;
	int rowNumber = 0;
			
	System.out.println("将要执行写操作, SQL 为:" + sql);
	try{
		statement = jdbcConn.createStatement();
		rowNumber = statement.executeUpdate(sql);
		
		if(sql.contains("INSERT INTO")){
			// 创建新纪录,返回id
			ResultSet rs =statement.getGeneratedKeys();
            if (rs.next()) {  
            	rowNumber = rs.getInt(1);   
//		                System.out.println("数据主键:" + id);   
            } 
		}
		
//			System.out.println("写操作成功!:::");
	}catch(Throwable e){
		System.out.println("写操作失败!:::" + sql);
		System.out.println(e.toString());
		
		e.printStackTrace();
	}finally{
		release(statement, null);
	}
	
	return rowNumber;
}
写操作远比查询简单,而且在一个方法中兼顾了 UPDATE 或 INSERT INTO 的操作。

释放资源

最后是释放 statement 对象和 ResultSet 对象资源,以免不及时释放带来内存占用过多的问题。这里,我特意设计其为一个通用的静态方法,无论查询的、还是写的方法,都可以通用。并且,更重要的是,不需要像 Connection 那样手动释放资源(您不需要手动执行 release() 这个方法),一切由框架为您代劳。这也是我设计该 DAO 的目标之一。

/**
 * 是否数据库资源
 * @param statement
 * @param resultset
 */
private static void release(Statement statement, ResultSet resultset){
	if(statement != null)
		try {
			statement.close();
		} catch (SQLException e) {}
	
	if(resultset != null)
		try {
			resultset.close();
		} catch (SQLException e) {}
}

附:经典的 JDBC 访问数据库方法

String sql = "SELECT * FROM User WHERE name = ?";
java.sql.Connection jdbcConn = ajaxjs.sql.DAO.getConn("jdbc/mysql_deploy");
java.sql.PreparedStatement statement = null;
java.sql.ResultSet rs = null;
String password = null;

try {
	statement = jdbcConn.prepareStatement(sql);
	statement.setString(1, UserName);
	System.out.println("开始查询数据库");
	rs = statement.executeQuery();
	if(rs.first()){
		
		// 有此用户,获取其密码
		password = rs.getString("password");
		// 保存登录用户信息
		User.put("name", rs.getString("name"));
		User.put("id", ((Integer)rs.getInt("id")).toString());
		
		System.out.println("数据库获取用户信息成功!" + rs.getString("name"));
		return password;
	}else{
		// 没有此用户
		throw new NullPointerException("没有此用户:" + UserName);
	}
} catch (SQLException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
	return null;
} catch (NullPointerException e){
	e.printStackTrace();
	throw e;
}finally{
	try {
		statement.close();
		rs.close();
		jdbcConn.close();
	} catch (SQLException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
}
参见:


Bigfoot 数据库 CRUD 举例

概述

Bigfoot DAO 旨在实现数据库的 CRUD 操作,目的是使用起来小巧、灵活、方便。使用 Bigfoot DAO 很简单,分三步实现。
第一步:建立数据库连接,框架支持两种数据库连接方式

  1.   通过硬编码指定 JDBC Url
  2.   通过 Tomcat 自带的数据库连接池产生 DataSource,进而获取 Connection 对象

如非测试,推荐使用第二种方法。

第二步:在以下选项中决定适合 CRUD 的组合,大多数情况下,推荐使用 UUID 主键、非事务和使用 PrepareStatement

  • boolean isTransactionEnable;            // 是否启用事务
  • boolean isUsingPreparedStatment;    // 是否使用 PreparedStatment
  • boolean isRETURN_GENERATED_KEYS;     // insert 是否返回自增 id
  • boolean isQueryAsSingle = true;                   // 如果 query() 只返回一条记录,则自动转化为但单独记录

第三步:创建 DBAccessImpl 对象,调用其方法

DBAccessImpl 实现了 DBAccess 接口,该接口目的为 CRUD 而生。

Result<Record> queryOne(String sql); // 查询单笔记录
Result<PureList> queryList(QueryRunner qr); // 查询多行记录
Result<PureList> queryList(String sql); // 查询多行记录
Result<PagedList> queryList(QueryRunner qr, int start, int limit); // 查询多行记录并分页
Result<CreateAction> insert(String tablename, IRecord data); // 插入记录,data 是 Map 的封装
Result<UpdateAction> update(String tablename, IRecord data, String uid);// 更新记录,data 是 Map 的封装
boolean delete(String tablename, String uid);// 指定表名和 id,删除一条记录

下面应用举例。

举例

查询单行、多行记录

DBAccess dao = new DBAccessImpl(conn);
Result<Record> oneResult = dao.queryOne("SELECT * FROM news WHERE uid = 100");// 可以是任意复杂的查询语句
Result<PureList> listResult = dao.queryList("SELECT * FROM news");

Result 是结果集对象,其中泛型 Record 表示单个实体,PureList 表示列表,但是就是普通结构的列表。

查询的实质是将任意查询结果 ResultSet 转化成 List<Map>。框架内提供了一个将每一条记录转换成一个 Map 对象的方法,通过循环语句将任意的查询结果 ResultSet 转换成 List<Map>类型的变量。

查询单行、多行记录,使用 QueryRunner

QueryRunner 是 SQL 语句字符串拼凑工具类,采用链式查询结构。

DBAccess dao = new DBAccessImpl(conn);
Result<PureList> result;
result = dao.queryList(new QueryRunner().select("uid").from("news"));
assertNotNull(result);
result = dao.queryList(new QueryRunner().select("uid").from("news").limit(0,  3));
assertNotNull(result);
result = dao.queryList(new QueryRunner().select("uid").from("news").limit(0,  3).orderBy("uid", "desc"));
System.out.println(result.json); // 返回 json
assertNotNull(result);

如上所示,演示了 SQL 的分页和排序功能,并直接提供 JSON 返回的能力。

插入一条记录

DBAccessImpl dao = new DBAccessImpl(conn);
dao.setUsingPreparedStatment(true);
MapHelper<String, IValue> map = new MapHelper<>();
map.put("uid", new Value(12345));
map.put("name", new Value("Roooss"));
dao.insert("news", map);
IRecord 是封装过 Map 的自定义类型,实现类为 MapHelper。只有键名等于表的字段名的数据才会插入到表中。

更新一条记录

DBAccessImpl dao = new DBAccessImpl(conn);
dao.setUsingPreparedStatment(true);
MapHelper<String, IValue> map = new MapHelper<>();
map.put("name", new Value("jack"));
dao.update("news", map, "12345");
插入或更新一组记录
请查询 public SaveAction save(String[] sqls, IRecord[] data) throws SQLException 文档,这里不再赘述了。

删除记录

DBAccessImpl dao = new DBAccessImpl(conn);
boolean isDeleted = dao.delete("news", "1000"); // 表名、 uid

约定

归纳一些基本的约定

  • 在 Map 变量(也就是封装过的 IRecord 类型)中,只有键名等于表的字段名的数据才会插入到表中。
  • 若无结果集,不返回零长度 List 对象,而是返回 null
  • 若无单个记录,不返回零长度 Map 对象,而是返回 null
  • 零长度字符串存入数据库中将保存为 null
  • DAO 通常会捕获了 SQLException 异常,不需要用户处理异常。

一点小技巧

最后,就是介绍大家一个我在框架内比较偷懒的办法。不论是查询,还是插入、更新记录,均使用 HashMap 对象作为一条记录的载体。这个 HashMap 对象的键名是表中的字段名,或是字段的别名,键值为字段值,键值的类型是字段所对应的 JDBC API 的 Java 类。针对数据库的操作,如:插入、更新、删除、查询,该方法会将所传递的 Map 型参数转化成 SQL 语句的组成部分,自动生成完整的标准的 SQL 语句,供 JDBC AP I调用。

实际上,Map 这种方法一般封装过 JDBC 的库多少都会支持的,并不是什么鲜为人知的秘密啦~


相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
1月前
|
Java 数据库连接
JDBC跨平台封装工具类
JDBC跨平台封装工具类
|
Java 数据库连接 数据库
JDBC学习(三):封装工具类JDBCUtils类
为了以后方便进行数据库连接、数据库关闭、还有后面的连接池的操作等功能,现在将功能封装到JDBCUtils类当中,作为自己的一个工具箱。
342 0
|
1月前
|
Java 关系型数据库 MySQL
flea-db使用之封装JDBC接入
【4月更文挑战第6天】本篇 Huazie 介绍 Flea 框架下的 flea-db 模块中封装JDBC的逻辑
24 1
flea-db使用之封装JDBC接入
|
3月前
|
SQL Java 数据库连接
JDBC如何封装成Mybaits持久层框架只需4
本文主要讲解JDBC怎么演变到Mybatis的渐变过程,重点讲解了为什么要将JDBC封装成Mybaits这样一个持久层框架。再而论述Mybatis作为一个数据持久层框架本身有待改进之处。
18 0
|
3月前
|
SQL druid Java
JDBC技术【分页查询、数据库连接池、应用程序分层、封装通用的BaseDao】(四)-全面详解(学习总结---从入门到深化)
JDBC技术【分页查询、数据库连接池、应用程序分层、封装通用的BaseDao】(四)-全面详解(学习总结---从入门到深化)
31 0
|
4月前
|
SQL Java 数据库连接
JDBC技术【分页查询、数据库连接池、应用程序分层、封装通用的BaseDao】(四)-全面详解(学习总结---从入门到深化)(下)
JDBC技术【分页查询、数据库连接池、应用程序分层、封装通用的BaseDao】(四)-全面详解(学习总结---从入门到深化)
408 1
|
4月前
|
SQL Java 数据库连接
JDBC技术【分页查询、数据库连接池、应用程序分层、封装通用的BaseDao】(四)-全面详解(学习总结---从入门到深化)(中)
JDBC技术【分页查询、数据库连接池、应用程序分层、封装通用的BaseDao】(四)-全面详解(学习总结---从入门到深化)
30 0
|
4月前
|
SQL 存储 Java
JDBC技术【封装JDBC工具类、Statement的使用、PreparedStatement的使用(重点)、ResultSet的使用】(二)-全面详解(学习总结---从入门到深化)
JDBC技术【封装JDBC工具类、Statement的使用、PreparedStatement的使用(重点)、ResultSet的使用】(二)-全面详解(学习总结---从入门到深化)
42 0
|
9月前
|
Java 数据库连接
Java 中封装JDBC连接到JDBCUtils工具类的详解
Java 中封装JDBC连接到JDBCUtils工具类的详解
43 0
|
9月前
|
SQL 安全 Java
JDBC之DAO层封装思想超详解(下)
JDBC之DAO层封装思想超详解(下)