本节为系列教程的第三部分: 真正的在线记账本: 客户端 + 服务器 + 数据库. 
经过前两节的努力, 我们已经实现了一个客户端 + 服务器模式的在线记账本, 但记账数据并未保存到数据库, 当服务端重启数据将丢失, 本节我们将实现将数据保存到数据库, 实现一个真正意义上的 “在线记账本”.
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 的文章.
 
    
~ 全剧终 ~ 
~ 点个关注呗 ~ 
~ 掌声、鲜花、小礼物走一走呗 ~
    
如果你发现此教程中有错误, 比如: 按照教程来做就是不对, 或者有些地方没有解释清楚. 麻烦你告诉我, 我改 !
即使你天生丽质, 自己已经把教程中的错误摆平, 也一定记得告诉我, 我改 ! 免得折腾后来的同学…