本篇记录了Java项目 ”一本糊涂账“ 的Debug开发日志。包括项目总结,整体架构复盘。
整体架构
”一本糊涂账“是一个Java + MySQL项目,对数据库中的数据进行简单统计和显示的项目。
主体架构分为:(1)数据库MySQL设计 (2)UI设计 (3)JDBC传递MySQL数据库与Java程序的接口
三层架构之间的关系为:UI中的数据与数据库MySQL中的数据通过JDBC进行交互
我们要解决的需求:
(1)需要记录每一笔消费,这笔消费要对应不同分类,当天日期,相应备注
(2)分类信息可以自行添加增减修改
(3)对消费记录信息进行简单统计,并用图标形式进行更改
(4)能够设置当月经费预算,并能够对数据库进行备份和恢复
针对以上主体架构和需求分析,我们需要构建的包如下:
- gui.frame => 对应UI设计主窗格
- gui.panel => 对应UI设计
- gui.service => 对应UI中相应的服务,在这里调用JDBC
- gui.listener => 对应于UI中相应的控件所触发的服务
- gui.model => UI中相关表格和相关数据统计可视化
- entity => 对应MySQL中相应的表结构,对应MySQL数据库中每一条数据在Java程序中的实例
- dao => JDBC实现,调用sql语句进行相应的数据库操作,并将数据库数据对应到entity实例
- util => 小功能实现,比如java.sql.Date与java.Util.Date之间的转化
- startup => 总程序入口,建立一个线程入口
项目总结复盘
数据库MySQL设计
如何构建数据库表结构是应该最先考虑的事情,也就是如何设置原始数据。后续的一切功能都需要建立在这个数据库上。
- 针对需求1:记录表格。记录每一笔消费,每一条记录具有属性 消费金额 , 消费类别 , 消费日期 , 备注 这四个属性。其中我们注意到消费类别是有限固定的,因此需要建立另一个表对应消费类别。这个消费类别对应另一个表中的主键数字。
1
2
3
4
5
6
7
8
9create table record(
id int auto_increment, #主键
spend int, # 消费金额
cid int, #消费类别
comment varchar(255), #备注
date Date, #消费日期
primary key(id),
constraint fk_record_category foreign key (cid) references category(id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8; - 针对消费类别:记录消费类别。
1
2
3
4
5create table category(
id int auto_increment, #主键
name varchar(255), #分类的名称
primary key(id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8; - 针对需求4:需要进行相关设置。建立一个表来配置相关设置
1
2
3
4
5
6create table config(
id int auto_increment, #主键
key_ varchar(255), #设置名称 key_
value varchar(255), #设置内容 value
primary key (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; - 相应的对应于Java中的entity数据实例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class Category { //对应消费类别
public int id;
public String name;
public int recordNumber; //这个其实没有用到
public method(){}
}
public class Config { //对应数据库设置
public int id;
public String key;
public String value;
public method(){}
}
public class Record { //对应每条消费记录
public int id;
public int spend;
public int cid;//对应category中的类别id
public String comment;
public Date date;
public method(){}
}相关sql建立
- 建立数据库并使用该数据库
1
2
3drop database if exists hutubill; #检查是否存在该数据库
create database hutubill; #如果不存在则创建该数据库
use hutubill; #定位到该数据库 - 定义表属性
1
2
3CREATE TABLE table_name (column_name column_type);
example:
`runoob_title` VARCHAR(100) NOT NULL #通过设置NOT NULL, 当输入数据位NULL的时候,mysql会报错 - 设置主键
1
2primary key (column_name) #设置主键
auto_increment #设置自增加,一般设置在主键上 - 设置存储引擎
1
2ENGINE 设置存储引擎, InnoDB是数据存储过后,关闭数据库数据仍然存在。
CHARSET 设置编码,utf-8编码包含了中文JDBC使用
JDBC是Mysql提供给Java语言的一个接口。主要目的是通过Java程序来操作Mysql数据库。
获取Mysql连接
1 | package util; |
JDBC的基本使用模式为:
1 | String sql = "insert into record values (NULL, ?, ?, ?, ?)"; |
使用经验:
- 尽量使用PreparedStatement,这样你可以轻而易举的控制能够加载sql语句中的那几个限定的类型,而不是自己将他转化成String的形式 https://www.runoob.com/mysql/mysql-data-types.html
- 在使用日期查询的过程中,sql语句中的date格式必须是
yyyy-mm-dd HH:MM:SS
. 比如在进行查询两个日期中间的数据时:String sql = "select * from record where date >= ? and date <= ? order by id desc";
如果使用ps.setDate(1, java.sql.Date date);
来进行填充,那么java.sql.Date只有yyyy-mm-dd没有时间信息。并且我们数据库里的是java.Util.Date。需要进行转换:java.sql.Date(java.sql.Date d.getTime())
。但转换过来就只有日期。
也可以使用ps.setTimeStamp(1, java.sql.TimeStamp(d.getTime()))
来进行填充,但是timeStamp是毫秒计数,转换出来的是yyyy-mm-dd HH:MM:SS.ms
。同样不符合要求。所以我选择了用字符串来填充的方式:1
2
3
4
5public static String util2sqlTimestamp(java.util.Date d){
DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.format(d);
return dateStr;
} - statement执行。
1
2
3
4
5
6
7
8
9Statement s = connection.createStatement(); //这里不加sql
s.execute(sql);
s.executeQuery(sql);
PreparedStatement = connection.preparedStatement(sql); //这里要加上sql
ps.execute();
ResultSet rs = ps.executeQuery();
注意如果需要返回ResultSet需要在statement中加入Statement.RETURN_GENERATED_KEYS - ResultSet返回
1
2
3rs.next(); //用来判断是否返回ResultSet
rs.getInt(); //填入列号或者字段名
rs.getString();UI设计,Service与Listener触发
1. UI设计
UI设计主要用到包含两个库:
- import javax.swing.JFrame; //用来创建主窗体,包含各种板面
- import javax.swing.JPanel; //用来创建各种板面,包含各种控件。
主窗体是整个程序的入口。一个程序的UI实际上就是在一个主窗体上不断变换各种显示面板。
创建单例模式 :
单例模式指的是在应用整个生命周期内只能存在一个实例。单例模式是一种被广泛使用的设计模式。他有很多好处,能够避免实例对象的重复创建,减少创建实例的系统开销,节省内存。
单例模式与静态类的区别 :首先单例模式是一个唯一存在的实例。静态类只是在程序编译的时候就创建了,可以不用创建实例就能进行调用。如果是一个非常重的对象,单例模式可以进行懒加载,静态类无法做到。如果只想使用一些方法或者变量,使用静态类在编译的过程中进行构建会比较快;但如果这个对象需要大量的后期维护,访问资源的时候,应该选择单例模式。
这里创建主窗体和板面都我们都用了单例模式:(都使用了饿汉模式加载:声明静态变量,在编译的时候构建对象, 缺点是占用资源,这种方式适合占用资源少,在初始化时候就能够用到的类。)
1 | 主窗体单例模式 |
在考虑板面交互的过程中,我们发现板面拥有共同的特征:
- 基本都需要对按键等控件添加监听器触发
- 基本都要进行数据的更新操作
因为我们抽象出一个WorkingPanel作为每一个板面的抽象类。注意继承抽象类的类必须实现所有的抽象类中定义为abstract的方法。在显示板面的时候,首先要更新板面数据。然后使用JPanel.updateUI()来进行板面更新。1
2
3
4public abstract class WorkingPanel extends JPanel{
public abstract void updateData(); //添加数据更新方法
public abstract void addListener(); //添加监听器
}
2. 控件监听器Listener
空间监听器是实现了ActionListener接口的类。需要实现actionPerformed方法,表示当该监听器被触发的时候做的一系列操作。
1 | public class ConfigListener implements ActionListener{ |
在板面的addListener方法中,通过如下方式添加监听器。
1 | protected void addListener() { |
一般情况下,在监听器被触发的条件下,都会产生相对应的操作。这些操作都会定义在Service类中,并放在监听器的actionPerformed方法中执行。
3. Service相应服务
对于本例来说,相应的服务都是对数据库进行相关操作,也就是数据更新,数据查询等操作。这个时候就会用到之前所定义的DAO类进行数据库相关操作。