数据库向来是个“老大难”问题。好的数据库设计与好的数据库程序密切相关,一个逻辑清楚、管理维护方便的程序往往离不开优秀的数据库表设计。本人之前就是基础不牢,对这个问题认识不够,忽视了数据库表设计,导致了后来程序层(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 开发效率不高,于是我们想想怎么封装一层吧!
大目标:轻量级,使数据库操作更简单、更顺手、更纯粹!
目标有大必有小。小的具体目标如下:
缺点是:一、通用性不会很强,只限于当前框架所使用,约定的关系较多,耦合紧;二、当前只针对 MySQL/SQLite。
之所以有这个基础模块,其实是排除了 DAO(Data Access Object) 所剩余的 SQL 包下面的内容。关于 DAO 模块,后面会论述。
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 还有释放资源的方法。
具体流程参见最新源码: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 编译器”,虽然有点贴切,但未免口气太大,与实际情况有点不符,想想这个名字还是算了。我的目标是这样的:应该是一种“半自动化 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 语句以下查询的功能:
此外,还有写操作的部分有待完善。
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 为只读的数据库访问类。
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 为写入的数据库访问类,大体可分为 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 满足了以下需求:
设计 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) {} }
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 DAO 旨在实现数据库的 CRUD 操作,目的是使用起来小巧、灵活、方便。使用 Bigfoot DAO 很简单,分三步实现。
第一步:建立数据库连接,框架支持两种数据库连接方式
如非测试,推荐使用第二种方法。
第二步:在以下选项中决定适合 CRUD 的组合,大多数情况下,推荐使用 UUID 主键、非事务和使用 PrepareStatement
第三步:创建 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");插入或更新一组记录
删除记录
DBAccessImpl dao = new DBAccessImpl(conn); boolean isDeleted = dao.delete("news", "1000"); // 表名、 uid
归纳一些基本的约定
最后,就是介绍大家一个我在框架内比较偷懒的办法。不论是查询,还是插入、更新记录,均使用 HashMap 对象作为一条记录的载体。这个 HashMap 对象的键名是表中的字段名,或是字段的别名,键值为字段值,键值的类型是字段所对应的 JDBC API 的 Java 类。针对数据库的操作,如:插入、更新、删除、查询,该方法会将所传递的 Map 型参数转化成 SQL 语句的组成部分,自动生成完整的标准的 SQL 语句,供 JDBC AP I调用。
实际上,Map 这种方法一般封装过 JDBC 的库多少都会支持的,并不是什么鲜为人知的秘密啦~