我正在制作一款财务管理应用程序。我有一个包含用户所有资金所在地(包括银行)的数据库。以下是表格的结构...
CREATE TABLE IF NOT EXISTS reserves (
id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(31) NOT NULL,
balance DECIMAL(10, 2) NOT NULL
)
CREATE TABLE IF NOT EXISTS banks (
reserve_id SMALLINT UNSIGNED UNIQUE NOT NULL,
apy DECIMAL(4, 2) NOT NULL,
accrued_interest DECIMAL(10, 4) NOT NULL,
last_transaction DATE,
FOREIGN KEY(reserve_id) REFERENCES reserves(id)
)
在这个模型中,我可以拥有一个固定的APY,它将在插入时设置。但在现实世界中,银行根据余额有可变的利率。而且对于银行表中的每个银行来说,具体情况都不同。
在Java类中,我可以非常容易地捕获这一点,将APY定义为
Function<BigDecimal,Big Decimal> APY
,在其中我可以存储特定的APY逻辑,并使用APY.apply(balance)
在任何时候检索利率。但我不知道如何在MySQL数据库中存储这个逻辑。
我知道我可以创建一个单独的表,比如bank_balance_interest,在其中我可以存储利率到最低余额到特定银行的id,然后引用它。
但这感觉并不正确。首先,它非常繁琐和乏味。此外,如果余额与利息没有明确的边界而是连续函数,则仍然没有解决方案。
是否有更优雅的方法?
以下是我的一些代码:
public class Reserve {
short id;
final String name;
BigDecimal balance;
ReservesData reservesData;
public Reserve(short id, String name, BigDecimal balance) {
this.id = id;
this.name = name;
this.balance = balance;
reservesData = ReservesData.instance;
}
public Reserve(String name) {
this((short) -1, name, new BigDecimal("0.0"));
}
@Override
public String toString() {
return name;
}
public short getId() {
return id;
}
public String getName() {
return name;
}
public BigDecimal getBalance() {
return balance;
}
public boolean transact(BigDecimal amount) {
if(balance.add(amount).compareTo(new BigDecimal("0.0")) < 0)
return false;
balance = balance.add(amount);
return true;
}
public boolean save() {
if(id == -1)
return (id = reservesData.addReserve(this)) != -1;
return reservesData.updateReserve(this);
}
}
public class Bank extends Reserve{
private final Function<BigDecimal, BigDecimal> APY;
private BigDecimal accruedInterest;
private Date lastTransactionDate;
private final BanksData banksData;
public Bank(short id, String name, BigDecimal balance, Function<BigDecimal, BigDecimal> APY) {
super(id, name, balance);
this.APY = APY;
accruedInterest = new BigDecimal("0.0");
banksData = BanksData.instance;
}
public Bank(String name, Function<BigDecimal, BigDecimal> APY) {
this((short) -1, name, new BigDecimal("0.0"), APY);
}
@Override
public BigDecimal getBalance() {
return balance.add(accruedInterest);
}
public Function<BigDecimal, BigDecimal> getAPY() {
return APY;
}
public BigDecimal getAccruedInterest() {
return accruedInterest;
}
public void setAccruedInterest(BigDecimal accruedInterest) {
this.accruedInterest = accruedInterest;
}
public class ReservesDAO implements ReservesData {
public ReservesDAO() {
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
stmt.executeUpdate("""
CREATE TABLE IF NOT EXISTS reserves (
id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(31) NOT NULL,
balance DECIMAL(10, 2) NOT NULL
)"""
);
} catch (SQLException sqlException) {
System.out.println("Failed to create reserves table on the database!");
sqlException.printStackTrace();
}
}
@Override
public short addReserve(Reserve reserve) {
try (
PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
INSERT INTO reserves (name, balance) VALUES (?, ?)""", Statement.RETURN_GENERATED_KEYS
)
) {
pstmt.setString(1, reserve.getName());
pstmt.setBigDecimal(2, reserve.getBalance());
pstmt.executeUpdate();
ResultSet rs = pstmt.getGeneratedKeys();
if (rs.next())
return rs.getShort(1);
else
throw new RuntimeException("Auto-Generated ID was not returned from reserves!");
} catch (SQLException sqlException) {
System.out.println("Failed to insert " + reserve.getName() + " info in the database!");
sqlException.printStackTrace();
return -1;
}
}
public Reserve getReserve(short id) {
try(
PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
SELECT * FROM reserves WHERE id = ?""")
) {
pstmt.setShort(1, id);
ResultSet rs = pstmt.executeQuery();
if(rs.next())
return new Reserve(rs.getShort(1), rs.getString(2), rs.getBigDecimal(3));
else throw new RuntimeException("No reserve found on the database with the id " + id);
} catch (SQLException sqlException) {
System.out.println("Failed to fetch reserve from the database!");
sqlException.printStackTrace();
return null;
}
}
public List<Reserve> getAllReserves() {
List<Reserve> reserves = new ArrayList<>();
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT * FROM reserves");
while(rs.next())
reserves.add(new Reserve(rs.getShort(1), rs.getString(2), rs.getBigDecimal(3)));
} catch (SQLException sqlException) {
System.out.println("Failed to fetch reserves from the database!");
sqlException.printStackTrace();
}
return reserves;
}
@Override
public BigDecimal getTotalReserveBalance() {
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("""
SELECT SUM(balance) FROM reserves""");
if(rs.next())
return rs.getBigDecimal(1);
return new BigDecimal("0.0");
} catch (SQLException sqlException) {
System.out.println("Could not get total reserve balance from database!");
sqlException.printStackTrace();
return null;
}
}
@Override
public List<Reserve> getAllWallets() {
List<Reserve> reserves = new ArrayList<>();
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("""
SELECT reserves.* FROM reserves
LEFT JOIN banks ON reserves.id = banks.id
WHERE banks.id IS NULL
""");
while(rs.next())
reserves.add(new Reserve(rs.getShort(1), rs.getString(2), rs.getBigDecimal(3)));
} catch (SQLException sqlException) {
System.out.println("Failed to fetch reserves from the database!");
sqlException.printStackTrace();
}
return reserves;
}
@Override
public BigDecimal getTotalWalletBalance() {
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("""
SELECT SUM(balance) FROM reserves
LEFT JOIN banks ON reserves.id = banks.id
WHERE banks.id IS NULL
""");
if(rs.next())
return rs.getBigDecimal(1) == null ? new BigDecimal("0.0") : rs.getBigDecimal(1);
return new BigDecimal("0.0");
} catch (SQLException sqlException) {
System.out.println("Could not get total wallet balance from database!");
sqlException.printStackTrace();
return null;
}
}
@Override
public boolean updateReserve(Reserve reserve) {
try(PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
UPDATE reserves SET name = ?, balance = ? WHERE id = ?""")
) {
pstmt.setString(1, reserve.getName());
pstmt.setBigDecimal(2, reserve.getBalance());
pstmt.setShort(3, reserve.getId());
pstmt.executeUpdate();
return true;
} catch(SQLException sqlException) {
System.out.println("Failed to update reserves with new data!");
sqlException.printStackTrace();
return false;
}
}
}
public class BanksDAO extends ReservesDAO implements BanksData {
public BanksDAO() {
try(
Statement stmt = MyConnection.getMySQLconnection().createStatement()
) {
stmt.executeUpdate("""
CREATE TABLE IF NOT EXISTS banks (
id SMALLINT UNSIGNED UNIQUE NOT NULL,
apy DECIMAL(4, 2) NOT NULL, // I have no way to store a logic here, so currently it only stores fixed value.
accrued_interest DECIMAL(10, 4) NOT NULL,
last_transaction_date DATE,
FOREIGN KEY(id) REFERENCES reserves(id)
)"""
);
} catch (SQLException sqlException) {
System.out.println("Failed to create banks table on the database!");
sqlException.printStackTrace();
}
}
@Override
public short addBank(Bank bank) {
try (
PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
INSERT INTO banks(id, apy, accrued_interest, last_transaction_date) VALUES (?, ?, ?, ?)"""
)
) {
short id = addReserve(bank);
pstmt.setShort(1, id);
pstmt.setBigDecimal(2, bank.getAPY());
pstmt.setBigDecimal(3, bank.getAccruedInterest());
pstmt.setDate(4, bank.getLastTransactionDate());
pstmt.executeUpdate();
return id;
} catch (SQLException sqlException) {
System.out.println("Failed to insert " + bank.getName() + " info in the database!");
sqlException.printStackTrace();
return -1;
}
}
@Override
public Bank getBank(short reserve_id) {
try(
PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
SELECT * FROM reserves NATURAL JOIN banks WHERE id = ?""")
) {
pstmt.setShort(1, reserve_id);
ResultSet rs = pstmt.executeQuery();
if(!rs.next())
return null;
Bank requestedBank = new Bank(rs.getShort(1), rs.getString(2),
rs.getBigDecimal(3), rs.getBigDecimal(4));
requestedBank.setAccruedInterest(rs.getBigDecimal(5));
requestedBank.setLastTransactionDate(rs.getDate(6));
return requestedBank;
} catch (SQLException sqlException) {
System.out.println("Failed to fetch bank data from the database!");
sqlException.printStackTrace();
return null;
}
}
@Override
public List<Bank> getAllBanks() {
List<Bank> allBanks = new ArrayList<>();
try(
Statement stmt = MyConnection.getMySQLconnection().createStatement()
) {
ResultSet rs = stmt.executeQuery("SELECT * FROM reserves NATURAL JOIN banks");
while(rs.next()) {
Bank bank = new Bank(rs.getShort(1), rs.getString(2),
rs.getBigDecimal(3), rs.getBigDecimal(4));
bank.setAccruedInterest(rs.getBigDecimal(5));
bank.setLastTransactionDate(rs.getDate(6));
allBanks.add(bank);
}
return allBanks;
} catch (SQLException sqlException) {
System.out.println("Failed to fetch bank data from the database!");
sqlException.printStackTrace();
return null;
}
}
@Override
public BigDecimal getTotalBankBalance() {
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("""
SELECT SUM(balance) FROM reserves NATURAL JOIN banks""");
if(rs.next())
return rs.getBigDecimal(1) == null ? new BigDecimal("0.0") : rs.getBigDecimal(1);
return new BigDecimal("0.0");
} catch (SQLException sqlException) {
System.out.println("Could not get total bank balance from database!");
sqlException.printStackTrace();
return null;
}
}
}
现在我可以这样初始化银行:
Bank bank1 = new Bank("TestBank1", balance -> balance.compareTo(new BigDecimal("10000")) == -1 ? new BigDecimal("4") : new BigDecimal("5"));
虽然我可以创建另一个银行,如下:
Bank bank2 = new Bank("TestBank2", balance -> balance.compareTo(new BigDecimal("8000")) == -1 ? new BigDecimal("3.5") : new BigDecimal("5.3"));
现在这两个银行都是在内存中创建的,并且只要应用程序在运行,它们就可以完美地工作。但是当我需要将其持久化以供长期使用时,我无法直接将类型为Function<BigDecimal, BigDecimal>的变量存储到MySQL数据库中。
许多人建议使用存储过程,如果只有一个逻辑,例如对于banks表中的每个银行都是
balance -> balance.compareTo(new BigDecimal("10000")) == -1 ? new BigDecimal("4") : new BigDecimal("5")
,那么这将起作用,但是这些信息会随着时间而改变。这意味着,如果我的banks表中有50个条目,则我需要为我的banks表中的每个条目创建50个不同的存储过程,以保持更新APY字段随余额更改而变化的方式。可能有更好的方法吗?