15.7 模拟银行存取款例程
【例程15-12】模拟银行存取款例程
这是一个模拟银行存取款操作的数据库示例程序,账户余额表和账户明细表的表结构前文已有介绍,下面是详细的程序源码。
15.7.1 创建数据库表例程
在博客:Java数据库编程之【概述】【SQL语言】【一】 中我们曾定义了一个创建数据库脚本bankDB_CrtDB.sql。 请参见 【例程15-1】 创建BankDB数据库的SQL脚本
在数据库的DBMS环境中执行一下脚本bankDB_CrtDB.sql,就会在路径“D:\\DB”下生成数据库。但是我们今天要演示的是Java程序调用脚本生成数据库。在程序中脚本要作为字符串导入,要稍作处理。下面是创建数据库表的演示程序共有二个类,类DbOperate用于管理SQL脚本、数据库名,以及数据库操作的程序调用;类DBConnectManager.java负责数据库创建、连接检测、错误信息处理等功能;
类DBConnectManager.java代码如下:
package bank; //程序DBConnectManager.java开始
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
public class DBConnectManager {
private static Connection con = null ;
/***由三部分一起完成数据库的连接: driver 是数据库驱动;
* dbProtocol指定数据库协议类型;
* dbName在嵌入模式中是带路径的数据库名;改为参数传入。
* 由dbProtocol和dbName组合成访问数据库的URL * ***/
private static final String driver = "org.apache.derby.jdbc.EmbeddedDriver" ;
private static final String dbProtocol = "jdbc:derby:" ;
//建立数据库连接
public static Connection GetConnection( String dbName ) throws SQLException {
if ( con != null && !con.isClosed() ) return con; //连接不为空,且没关闭.
else if (connectDb(dbName)) { return con; } //尝试建立数据库连接
else {
System.err.println("数据库不存在!数据连接失败。 ");
return con;
}
}
//根据建表参数,新建数据库表
public static void CreateDbTable(String dbName, String createTableSQL)
{
/***建立数据库连接url: "create=true"表示如果数据库不存在,就建立数据库 ***/
String url = dbProtocol + dbName + ";create=true";
try {
if (con==null) { //如果数据库连接不存在,建立数据库连接
Class.forName(driver) ; /*加载数据库驱动*/
//建立数据库连接
con = DriverManager.getConnection(url);
}
Statement stmt = con.createStatement();
stmt.executeUpdate(createTableSQL) ;
stmt.close();
}catch (SQLException se) {
PrintSQLException(se) ;
}catch(ClassNotFoundException e){
System.err.println("JDBC驱动:" + driver + " 不在CLASSPATH路径里") ;
}
}
//建立数据库连接,成功返回true,否则返回false。
public static boolean connectDb(String dbName) {
String url = dbProtocol + dbName;
boolean rnflg = false ;
try{
Class.forName(driver) ;
con = DriverManager.getConnection( url );
SQLWarning swarn = con.getWarnings();
if(swarn != null) PrintSQLWarning(swarn);
rnflg = true ;
} catch (SQLException se) {
PrintSQLException(se) ;
}catch(ClassNotFoundException e){
System.err.println("JDBC驱动:" + driver + " 不在CLASSPATH路径里") ;}catch(Exception e){ } // Do nothing, as DB does not (yet) exist
return(rnflg) ;
}
/***打印SQLException异常出错信息的方法***/
public static void PrintSQLException(SQLException se) {
while(se != null) {
System.out.print("SQLException: State: " + se.getSQLState());
System.out.println(" Severity: " + se.getErrorCode());
System.out.println(se.getMessage());
se = se.getNextException();
}
}
/***打印SQLWarning警告信息的方法***/
public static void PrintSQLWarning(SQLWarning sw) {
while(sw != null) {
System.out.print("SQLWarning: State=" + sw.getSQLState()) ;
System.out.println(" Severity = " + sw.getErrorCode()) ;
System.out.println(sw.getMessage());
sw = sw.getNextWarning();
}
}
} //程序DBConnectManager.java结束。
类DbOperate.java代码如下:
package bank; //程序DbOperate.java开始
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
public class DbOperate {
//绝对路径的数据库名,将在指定的路径创建数据库。如用相对路径则数据库创建在工作目录下
private static final String dbName = "D:\\\\DB\\\\BankDB";
/***创建银行账户余额表bank.actsBalance的SQL语句脚本***/
private static final String CrtActsBalanceTableSQL =
"CREATE TABLE bank.actsBalance ( " +
"cardNumber VARCHAR(19) PRIMARY KEY," +
"actBalance DECIMAL(10,2) check(actBalance>=0)," +
"tradeDate TIMESTAMP )" ;
/***创建银行账户明细表bank.actsDetail的SQL语句脚本***/
private static final String CrtActsDetailTableSQL =
"CREATE TABLE bank.actsDetail ( " +
"cardNumber VARCHAR(19) NOT NULL," +
"deposit DECIMAL(10,2) check(deposit>0)," +
"withdraw DECIMAL(10,2) check(withdraw>0)," +
"tradeDate TIMESTAMP, " +
"FOREIGN KEY (cardNumber) REFERENCES bank.actsBalance(cardNumber) )" ;
/***插入演示账户数据***/
private static final String InitTableSQL =
"INSERT INTO bank.actsBalance(cardNumber, actBalance, tradeDate) " +
" VALUES ('6223161000568866778',0.00,'2020-01-12 08:56:30')," +
" ('6223161056881238',0.00,'2020-01-13 08:16:26')," +
" ('6223160000568866',0.00,'2020-01-14 12:20:16')";
/***查询账户余额***/
private static final String QueryBalanceSQL =
"SELECT * FROM bank.actsBalance " +
"WHERE 1=1";
//查询余额表中数据记录
static void doQueryBalance(String sql) throws SQLException {
SQLWarning swarn = null ;
Connection con = DBConnectManager.GetConnection(dbName);
try (Statement stmt = con.createStatement() ) { //自动关闭资源try语句
stmt.execute(sql) ;
ResultSet rs = stmt.getResultSet();
while(rs.next()) {
System.out.print("cardNumber: " + rs.getString("cardNumber")) ;
System.out.print("\\tactBalance: " + rs.getBigDecimal("actBalance")) ;
System.out.println("\\ttradeDate: " + rs.getTimestamp("tradeDate") );
swarn = rs.getWarnings() ;
if(swarn != null) DBConnectManager.PrintSQLWarning(swarn);
}
}catch (SQLException se) { DBConnectManager.PrintSQLException(se) ; }
}
//执行静态更新类SQL命令对更新数据库
public static void doUpdateStatement( String sql ) throws SQLException
{
Connection con = DBConnectManager.GetConnection(dbName);
try {
Statement stmt = con.createStatement() ;
stmt.executeUpdate(sql);
}catch (SQLException se) { DBConnectManager.PrintSQLException(se) ; }
}
public static void main(String[] args) throws SQLException {
if (DBConnectManager.connectDb(dbName)==false)
{ //连接失败,新建数据库
DBConnectManager.CreateDbTable(dbName, CrtActsBalanceTableSQL);
DBConnectManager.CreateDbTable(dbName, CrtActsDetailTableSQL);
doUpdateStatement(InitTableSQL);
System.out.println("数据库创建完毕!");
}
doQueryBalance(QueryBalanceSQL);
System.out.println("程序运行完毕!");
}
} //程序DbOperate.java结束
编译运行例程DbOperate,程序初次运行时的测试效果图:
上图是程序正常运行效果,数据库及二个表已成功建立。程序运行前初始状态没有数据库目录“D:\\DB\\BankDB”,程序执行后数据库自动建立在“D:\\DB\\BankDB”目录里。
如果显示“JDBC驱动: org.apache.derby.jdbc.EmbeddedDriver 不在CLASSPATH路径里”,表示程序找不到数据库derby驱动程序。解决方法如下面说明所示。
说明:编写derby数据库程序时有二种方式指示数据库驱动的路径,一种是把derby.tar的路径配置在CLASSPATH里;另一种是如下图,通过“增加外部JAR包(Add External JARs…)”的方式,配置到构建路径(Build Path)中。
15.7.2 增加存款和取款等功能
然后,再给class DbOperate增加功能,先增加如下的SQL语句定义:
/***按账号用动态SQL语句查询账户余额***/
private static final String QueryBalanceSQL3 =
"SELECT * FROM bank.actsBalance " +
"WHERE cardNumber = ? ";
/***查询账户明细***/
private static final String QueryDetailSQL =
"SELECT cardNumber,deposit,withdraw,tradeDate " +
"FROM bank.actsDetail ";
/***更新账户余额表的SQL语句脚本***/
private static final String UpdateBalanceSQL =
"UPDATE bank.actsBalance SET actBalance=? , tradeDate=? " +
"WHERE cardNumber=? ";
/***取款的SQL语句脚本***/
private static final String DepositSQL =
"INSERT INTO bank.actsDetail(cardNumber, deposit, tradeDate) " +
"VALUES(?, ?, ?)" ;
/***存款的SQL语句脚本***/
private static final String WithdrawSQL =
"INSERT INTO bank.actsDetail(cardNumber, withdraw, tradeDate) " +
"VALUES(?, ?, ?)" ;
再给class DbOperate增加功能,然后再增加如下的方法定义:
//执行动态SQL语句(预编译SQL语句)数据库DML更新的方法
public static int DoPstmt(String actNo,BigDecimal amount ,Timestamp stamp,String sql) throws SQLException {
int num = 0;
Connection con =DBConnectManager.GetConnection(dbName);
try (PreparedStatement pstmt = con.prepareStatement(sql) )
{
pstmt.clearParameters() ;
pstmt.setString(1, actNo) ;
pstmt.setBigDecimal(2, amount) ;
pstmt.setTimestamp(3, stamp); //设置时间戳
num = pstmt.executeUpdate();
} catch (SQLException e) {
DBConnectManager.PrintSQLException(e);
}
return num;
}
//存款方法Deposit()
public static void Deposit(String actNo,BigDecimal amount ,Timestamp stamp) throws SQLException {
boolean flg = true;
UpdateBalance(actNo, amount, stamp, flg); //更新账户余额表
DoPstmt( actNo, amount , stamp, DepositSQL); //更新账户明细雨表
}
//取款方法Withdraw()
public static void Withdraw(String actNo,BigDecimal amount ,Timestamp stamp) throws SQLException {
boolean flg = false;
UpdateBalance(actNo, amount, stamp, flg); //更新账户余额表
DoPstmt( actNo, amount , stamp, WithdrawSQL); //更新账户明细雨表
}
//更新余额表账户余额方法UpdateBalance()
public static void UpdateBalance(String actNo,BigDecimal amtt ,Timestamp stamp,boolean flag) throws SQLException {
Connection con = null;
BigDecimal balance = BigDecimal.valueOf(0.0) ; //余额
BigDecimal newbalance = BigDecimal.valueOf(0.0) ; //新余额
System.out.println("UpdateBalance方法Begin:" );
//先查询账户余额
con =DBConnectManager.GetConnection(dbName);
try (PreparedStatement pstmt = con.prepareStatement(QueryBalanceSQL3) )
{
pstmt.clearParameters() ;
pstmt.setString(1, actNo) ;
ResultSet resultSet = pstmt.executeQuery(); //执行数据库查询
if (resultSet.next())
balance = resultSet.getBigDecimal("actBalance"); //得到账户更新前余额
} catch (SQLException e) {
DBConnectManager.PrintSQLException(e);
}
if (flag) { //flag = true 存款
newbalance = balance.add(amtt); //加上存款额
} else { //flag = false 取款
newbalance = balance.subtract(amtt); //减去取款额
}
System.out.println("新余额:"+newbalance +"发生额:"+amtt);
//更新余额表中的账户余额
con =DBConnectManager.GetConnection(dbName);
try (PreparedStatement pstmt = con.prepareStatement(UpdateBalanceSQL) )
{
pstmt.clearParameters() ;
pstmt.setBigDecimal(1, newbalance) ; //设置新余额
pstmt.setTimestamp(2, stamp); //设置时间戳
pstmt.setString(3, actNo) ;
pstmt.executeUpdate();
} catch (SQLException e) {
DBConnectManager.PrintSQLException(e);
}
System.out.println("UpdateBalance方法end:" );
}
//查询明细表中的数据记录cardNumber,deposit,withdraw,tradeDate
static void doQueryDetail(String sql) throws SQLException {
SQLWarning swarn = null ;
Connection con = DBConnectManager.GetConnection(dbName);
try (PreparedStatement pstmt = con.prepareStatement(sql) ) { //自动关闭资源try语句
pstmt.executeQuery();
ResultSet rs = pstmt.getResultSet();
BigDecimal je = null;
while(rs.next()) {
System.out.print("cardNumber: " + rs.getString("cardNumber")) ;
je = rs.getBigDecimal("deposit");
System.out.print("\\tdeposit: " + ( (je==null)? " " : je ) ) ;
je = rs.getBigDecimal("withdraw");
System.out.print("\\twithdraw: " + ( (je==null)? " " : je ) ) ;
System.out.println("\\ttradeDate: " + rs.getTimestamp("tradeDate") );
swarn = rs.getWarnings() ;
if(swarn != null)
DBConnectManager.PrintSQLWarning(swarn);
}
}catch (SQLException se) {
DBConnectManager.PrintSQLException(se) ;
}
}
在class DbOperate新增加功能的以上代码后,类源文件中应当已自动添加了以下的类导入语句:
import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.Timestamp;
最后在main方法的“System.out.println(“程序运行完毕!”);”前增加如下语句:
System.out.println("———————————————");
BigDecimal je = BigDecimal.valueOf(200.0);
Timestamp tmstamp = new Timestamp(System.currentTimeMillis());
Withdraw("6223161000568866778",je , tmstamp); //取款
tmstamp = new Timestamp(System.currentTimeMillis());
Deposit("6223161000568866778", BigDecimal.valueOf(100.56), tmstamp);
System.out.println("———————————————");
doQueryBalance(QueryBalanceSQL);
System.out.println("———————————————");
doQueryDetail(QueryDetailSQL);
新增的代码实际上做了以下这几个操作:
1,从账户"6223161000568866778"取款200元的操作:
Withdraw(“6223161000568866778”,je , tmstamp);
2,往账户"6223161000568866778"存款100.56元的操作:
Deposit(“6223161000568866778”, BigDecimal.valueOf(100.56), tmstamp);
3,查询账户余额表中的信息:
doQueryBalance(QueryBalanceSQL);
4,查询账户明细表中的信息
doQueryDetail(QueryDetailSQL);
由于银行借记卡的余额必须 > =0 。再次执行程序DbOperate.java,会有如下错误提示:
程序调通后,你可按自己的意愿进行修改,在账户余额表中增加新的账户记录,或设计一些存款、取款交易,来查看分析程序结果。
如你做存款或取款时,金额使用负值,也会出现类似上面的错误。这些错误说明程序输入的数据有问题。比如,取款金额如果是-88.60元,或者账户余额只有60元,你想取500元,这交易都不会成功。报错是理所当然的。
银行取款交易正常的情况下,如果交易成功。账户余额表中的余额要减去取款金额,同时账户明细表中要增加一条取款交易的记录;存款交易也类似,只是对账户余额的处理是加上存款金额,方向相反。但本例中取款金额大于账户余额,发生数据库操作异常,账户余额更新失败了,但账户明细表中的记录还是增加了。这就会产生脏数据,导致账户不平。下一节将优化解决这个问题。
15.7.3 数据库账户处理程序优化
上面的程序还存在一个问题,以上面显示的这错误信息分析,账户的余额为0,客户要取款200元,余额不足。银行交易是不会成功的。但上面的程序中虽然余额表中更新记录没成功,但账户明细表中却插入了一条取款200元的记录(这笔交易实际没成功,不应当增加这条取款记录)。下面通过增加条件判断来优化一下,解决这个问题。
主要优化了三个方法,三个方法原来都是viod类型方法没有返回值,现在根据需要都更新为boolean类型,方法都返回一个条件值,返回true表示,交易成功或正常操作,如果返回false,则表示操作异常或交易失败。存款和取款方法,都要检查账户余额,如果账户余额是负值,直接报交易失败,并退出方法;如果账户余额为正值,再调用更新余额表方法,如果取款时发现余额不足,将显示存款交易失败,退出方法。
更新账户余额表方法UpdateBalance,如余额不足将返回false,正常时返回true。三个方法优化后代码如下:
//存款方法Deposit()
public static boolean Deposit(String actNo,BigDecimal amount ,Timestamp stamp) throws SQLException {
boolean rtn = false;
boolean flg = true; //true表示存款
if (amount.compareTo(BigDecimal.ZERO)==1) { //发生amount>0
rtn = UpdateBalance(actNo, amount, stamp, flg); //更新账户余额表
if (rtn) { //如果rtn=true,则登记存款分户账
DoPstmt( actNo, amount , stamp, DepositSQL); //增加账户明细记录
System.out.println("存款交易,交易成功!" );
return true;
}
else { //存款交易,余额不足,永远不会发生。
System.out.println("存款,交易失败,余额不足!" );
return false;
}
}
else { //发生额amount<0
System.out.println("存款交易失败!发生额不能为负值:amount="+amount );
return false;
}
}
//取款方法Withdraw()
public static boolean Withdraw(String actNo,BigDecimal amount ,Timestamp stamp) throws SQLException {
boolean rtn = false;
boolean flg = false; //false表示存款
if (amount.compareTo(BigDecimal.ZERO)==1) { //发生额amount>0
rtn = UpdateBalance(actNo, amount, stamp, flg); //更新账户余额表
if (rtn) { //如果rtn=true,则登记存款分户账
DoPstmt( actNo, amount , stamp, WithdrawSQL); //增加明细表取款记录
System.out.println("取款交易,交易成功!" );
return true;
}
else{ //取款交易,余额不足
System.out.println("取款,交易失败,余额不足!" );
return false;
}
}
else { //发生额amount<0
System.out.println("取款交易失败!发生额不能为负值:amount="+amount );
return false;
}
}
//更新余额表账户余额方法UpdateBalance()
public static boolean UpdateBalance(String actNo,BigDecimal amtt ,Timestamp stamp,boolean flag) throws SQLException {
Connection con = null;
BigDecimal balance = BigDecimal.valueOf(0.0) ; //余额
BigDecimal newbalance = BigDecimal.valueOf(0.0) ; //新余额
System.out.println("UpdateBalance方法Begin:" );
//先查询账户余额
con =DBConnectManager.GetConnection(dbName);
try (PreparedStatement pstmt = con.prepareStatement(QueryBalanceSQL3) )
{
pstmt.clearParameters() ;
pstmt.setString(1, actNo) ;
ResultSet resultSet = pstmt.executeQuery(); //执行数据库查询
if (resultSet.next())
balance = resultSet.getBigDecimal("actBalance"); //得到账户更新前余额
} catch (SQLException e) {
DBConnectManager.PrintSQLException(e);
}
if (flag) { //flag = true 表示存款
newbalance = balance.add(amtt); //加上存款额
} else { //flag = false 表示取款
newbalance = balance.subtract(amtt); //减去取款额
}
System.out.println("新余额:"+newbalance +"发生额:"+amtt);
if (newbalance.compareTo(BigDecimal.ZERO)==–1) { //余额不足,小于0
return false;
}
//更新余额表中的账户余额
con =DBConnectManager.GetConnection(dbName);
try (PreparedStatement pstmt = con.prepareStatement(UpdateBalanceSQL) )
{
pstmt.clearParameters() ;
pstmt.setBigDecimal(1, newbalance) ; //设置新余额
pstmt.setTimestamp(2, stamp); //设置时间戳
pstmt.setString(3, actNo) ;
pstmt.executeUpdate();
} catch (SQLException e) {
DBConnectManager.PrintSQLException(e);
}
return true;
}//更新余额表账户余额方法UpdateBalance()结束。
程序调通后,删除数据库D:\\DB\\BankDB,你再从头开始,相当于重新初始化数据库环境。再按自己的想法修改main方法中的存取款交易金额试试,在账户余额表中增加新的账户记录,或设计一些存款、取款交易,来查看分析程序结果。
应当不会再出现违反数据库约束的异常。如果条件不正确,将报交易失败,并不会更新数据库,账户是平的。
测试记录,初始余额是0.0元,存款二笔,取款一笔,最终余额是140.60元。
说明:以上是单用户模式的应用场景。如是多线程多用户场景,还要在操作数据库记录时增加加锁功能。
评论前必须登录!
注册