数据库向来是个“老大难”问题。好的数据库设计与好的数据库程序密切相关,一个逻辑清楚、管理维护方便的程序往往离不开优秀的数据库表设计。本人之前就是基础不牢,对这个问题认识不够,忽视了数据库表设计,导致了后来程序层(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 result = null;
/**
* 已转换为 Map 结果集合,如果单数则是 null
*/
public Map[] 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 pair);
public int update(Map 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[] 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 pair = new HashMap();
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
例子如下(摘自单元测试)。
@Test
public void updateByMap(){
Writer writer = new Writer(conn);
writer.setTableName(ReaderTest.tableName);
Map pair = new HashMap();
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 pair = new HashMap();
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
其中,回调的方法是强大、灵活的方法。现在很多接口都是 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 queryOne(String sql); // 查询单笔记录
Result queryList(QueryRunner qr); // 查询多行记录
Result queryList(String sql); // 查询多行记录
Result queryList(QueryRunner qr, int start, int limit); // 查询多行记录并分页
Result insert(String tablename, IRecord data); // 插入记录,data 是 Map 的封装
Result update(String tablename, IRecord data, String uid);// 更新记录,data 是 Map 的封装
boolean delete(String tablename, String uid);// 指定表名和 id,删除一条记录
下面应用举例。
查询单行、多行记录
DBAccess dao = new DBAccessImpl(conn);
Result oneResult = dao.queryOne("SELECT * FROM news WHERE uid = 100");// 可以是任意复杂的查询语句
Result listResult = dao.queryList("SELECT * FROM news");
Result 是结果集对象,其中泛型 Record 表示单个实体,PureList 表示列表,但是就是普通结构的列表。
查询的实质是将任意查询结果 ResultSet 转化成 List
查询单行、多行记录,使用 QueryRunner
QueryRunner 是 SQL 语句字符串拼凑工具类,采用链式查询结构。
DBAccess dao = new DBAccessImpl(conn);
Result 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 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 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 的库多少都会支持的,并不是什么鲜为人知的秘密啦~