Java数据库编程之【JDBC数据库例程】【模拟银行存取款例程】【八】

发布于:2025-08-14 ⋅ 阅读:(15) ⋅ 点赞:(0)

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元。
说明:以上是单用户模式的应用场景。如是多线程多用户场景,还要在操作数据库记录时增加加锁功能。


网站公告

今日签到

点亮在社区的每一天
去签到