本节为系列教程的第三部分: 真正的在线记账本: 客户端 + 服务器 + 数据库.
经过前两节的努力, 我们已经实现了一个客户端 + 服务器模式的在线记账本, 但记账数据并未保存到数据库, 当服务端重启数据将丢失, 本节我们将实现将数据保存到数据库, 实现一个真正意义上的 “在线记账本”.
12. 创建数据库
本教程中我们使用 MySQL, 当然, 你也可以使用其它的 DBMS, 如: Microsoft SQL Server 等.
这只需要使用相应的 JDBC 驱动, 并变换一下连接参数即可 (见下文).
MySQL 的安装与配置就不赘述了, 问度娘~
创建一个名为 account_book 的数据库 (Schema):
1
| create database account_book;
|
在 account_book
数据库中创建基本表 bills
, 用于存储账单数据:
1 2 3 4 5 6 7
| CREATE TABLE `bills` ( `id` int(11) NOT NULL AUTO_INCREMENT, `time` datetime NOT NULL, `amount` int(11) NOT NULL, `memo` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8;
|
注意观察 bills 表的结构, 它们与此前我们客户端/服务器端的数据模型是一致的.
13. 对接数据库
13.1 准备数据库驱动
我们将使用 JDBC 连接 MySQL 数据库, 所以先得 下载 MySQL 的驱动程序.
然后, 将此驱动程序文件 (.jar) 放到上节创建的 WEB-INF/lib
文件夹下 ( 和上节 web-lighter 的那些 jar 文件放在一起 )
最后, 在 IDEA 中驱动程序文件上点击右键, 右键菜单中选择 Add As Library…, 将其添加到构建路径.
完成后的 WEB-INFO 文件夹结构大概像这样:
13.2 创建数据库工具类
与数据库交互时有很多参数和步骤是相同的, 为了统一管理, 我们先来创建一个数据库工具类.
Code-13.2: DBUtil.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package example.dao;
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException;
public class DBUtil { final private static String driver = "com.mysql.jdbc.Driver"; final private static String url = "jdbc:mysql://localhost/account_book?autoReconnect=true"; final private static String user = "root"; final private static String password = "1234";
public static Connection getConnection() throws ClassNotFoundException, SQLException { Class.forName(driver); return DriverManager.getConnection(url, user, password); } }
|
DBUtil
类将数据库连接参数信息统一管理, 此外还提供了一个公有 (public) 静态(static) 方法getConnection()
返回数据库连接对象.
这里的 DBUtil 只对取得数据库连接对象(Connection)这一功能进行的封装. 在未来实践的过程中你可能会意识到可以把更多的功能 (如: 查询/更新) 封装到 DBUtil 里.
注意: 第 12, 14, 16 行中的数据库名, 登录名, 密码改成你自己的!
13.3 创建数据库访问类
现在让我们把跟数据库交互的相关操作都封装起来, 创建如下AccountBookDAO
类:
对照代码中的注释慢慢看…
本人有点懒, 若前面注释过的内容, 后面再出现就没再注释了, 所以请按顺序看.
Code-13.3: AccountBookDAO.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
| package example.dao;
import example.vo.Bill; import java.sql.*; import java.util.ArrayList; import java.util.List;
public class AccountBookDAO {
public List<Bill> queryBills() throws SQLException, ClassNotFoundException { Connection conn = null; Statement stmt = null; ResultSet rs = null; List<Bill> bills = new ArrayList<>(); try { conn = DBUtil.getConnection(); stmt = conn.createStatement(); rs = stmt.executeQuery("select * from bills order by time desc;"); while (rs.next()) { bills.add(packBill(rs)); } } finally { if (rs != null) rs.close(); if (stmt != null) stmt.close(); if (conn != null) conn.close(); } return bills; }
public Bill saveBill(Bill bill) throws SQLException, ClassNotFoundException { Connection conn = null; PreparedStatement pstm = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); String sql = "insert into bills(time, amount, memo) values (?, ?, ?)"; pstm = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); Timestamp now = new Timestamp(System.currentTimeMillis()); pstm.setTimestamp(1, now); pstm.setLong(2, bill.getAmount()); pstm.setString(3, bill.getMemo()); pstm.executeUpdate(); rs = pstm.getGeneratedKeys(); if (rs.next()) { bill.setId(rs.getInt(1)); bill.setTime(now); } else { throw new SQLException("新增数据失败"); } } finally { if (rs != null) rs.close(); if (pstm != null) pstm.close(); if (conn != null) conn.close(); } return bill; }
public void removeBill(Bill bill) throws SQLException, ClassNotFoundException { Connection conn = null; PreparedStatement pstm = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); String sql = "delete from bills where id = ?"; pstm = conn.prepareStatement(sql); pstm.setInt(1, bill.getId()); pstm.execute(); } finally { if (pstm != null) pstm.close(); if (conn != null) conn.close(); } }
private Bill packBill(ResultSet rs) throws SQLException { Bill bill = new Bill(); bill.setId(rs.getInt("id")); bill.setTime(rs.getTimestamp("time")); bill.setAmount(rs.getLong("amount")); bill.setMemo(rs.getString("memo")); return bill; } }
|
上述AccountBookDAO
实现了对账单数据进行”查询/插入/删除”的封装. 修改功能并未实现, 留给你来完成.
此外, 请注意: AccountBookDAO 与外界的接口部分转递的信息是最简单朴素的 Bill
或 List<Bill>
. 也就是说, 我们要尽可能地让 AccountBookDAO 之外的世界不知道数据库和 JDBC 的存在, 也无需关心数据是怎么来的, 怎么保存的, 这同样是分层解耦的思想.
14. 最后一步, 大功告成!
最后我们来修改 BillAction.java 中的代码, 让它来与上面定义的 AccountBookDAO 对接, 而不是对接AccountBookService.
Code-14.1: BillAction.java (修改后)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package example.action;
import com.bailey.web.lighter.action.ActionResult; import com.bailey.web.lighter.action.ActionSupport; import com.bailey.web.lighter.annotation.Inject; import com.bailey.web.lighter.annotation.Param; import com.bailey.web.lighter.annotation.Request; import example.dao.AccountBookDAO; import example.service.AccountBookService; import example.vo.Bill;
public class BillAction extends ActionSupport { @Request(url = "/getBills") public ActionResult getBills(@Inject AccountBookDAO dao) { try { return ActionResult.success(dao.queryBills()); } catch (Exception e) { return ActionResult.failure(); } }
@Request(url = "/saveOrUpdateBill") public ActionResult saveOrUpdateBill( @Inject AccountBookDAO dao, @Param(name = "action") String action, @Param(name = "bill") Bill bill) {
try { switch (action) { case "append": bill = dao.saveBill(bill); break; case "remove": dao.removeBill(bill); break; } return ActionResult.success(bill); } catch (Exception e) { return ActionResult.failure(); } } }
|
再开一个浏览器窗口, 打开第 (二)节教程, 仔细对比上面的代码(Code-14.1)和原先的代码(Code-10.1)的变化.
其实, 关键的变化是我们把服务器端数据模型的交互操作从 AccountBookService 切换成了 AccountBookDAO.
这又是分层设计的优势的另一体现
重启一下服务器端 Tomcat, 浏览器里一顿操作之后是不是发现数据都已经保存到数据库里了.
此时, 即使重启服务器, 你的账本数据都还好好的在那里等着你. 这就是”真正的在线记账本”!
这回大家开心了吧~
按照惯例我们还是来小结一下:
本节内容其实不多, 只是在第(二)节基础上把数据的存储位置切换到了数据库里. 问题的关键在于:
分层、低耦合的设计将使得程序变得条理清晰, 而又灵活. 在需要撤换某一层时, 低耦合的设计将使得这项工作简单且平顺地完成.
反思 AccountBookDAO.java (Code-13.3) 的代码, 你也许会发现它并不那么优雅, 甚至有点点臃肿, 乱!
比如:
那些 SQL 语句能否不用手工来写, 如果有工具能帮我们自动生成所需的 SQL 该有多好!
那个packBill()
方法的作用在于把数据库查询结果 ResultSet 中的数据库封装成简单的值对象(bill, VO) , packBill() 里大量的代码都在做搬运工. 能否有工具能帮我们完成这个封装工作, 并维护 bill 的状态?
严格来说, 与数据库中记录对应的 Java 对象应称作持久层对象(PO, Persistant Object). 在实践中, PO 除数据外可能还有状态等其它信息.
本例为了简单起见, 不想把事情搞大, 所以将就用一下此前定义的那个 VO (Value Object)
类似的问题还很多, 程序写得多了, 你就会有强烈的愿望, 想要有个东西来帮你完成与数据库交互这一层的封装,这就是所谓的持久层封装.
呵呵, 那还等什么?
继续学习ORM (Object Relational Mapping), 持久层框架 (如: Hibernate, myBatis, Cayenne…) , 它们就是你想要的.
当然, 本人推荐 Cayenne, 本博客里好几篇介绍 Cayenne 的文章.
~ 全剧终 ~
~ 点个关注呗 ~
~ 掌声、鲜花、小礼物走一走呗 ~
如果你发现此教程中有错误, 比如: 按照教程来做就是不对, 或者有些地方没有解释清楚. 麻烦你告诉我, 我改 !
即使你天生丽质, 自己已经把教程中的错误摆平, 也一定记得告诉我, 我改 ! 免得折腾后来的同学…