I am making a finance management application. I have a database containing all the places the user has his/her money which includes banks. Here is how the table is structured...
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)
)
In this model I can have a fixed APY which will be set while insertion. But in the real world, banks have variable interest rates based on the balance. And the specifics are different for every bank in the banks table.
In the Java class I can capture this very easily with the APY defined as a Function<BigDecimal, Big Decimal> APY where I can store the specific APY logic and use APY.apply(balance) to retrieve the interest rate at any point.
But I don't know how to store this logic in a MySQL database.
I know I can create a separate table like bank_balance_interest where I can store the interest rate to the minimum balance to the id of the specific bank and then refer to that.
But it just does not feel right. For one, it is very cumbersome and tedious. Besides there still won’tbe any solution if there aren’t any clear boundaries for balance to interest and rather it is a continuous function.
Is there a more elegant approach?
Here is some of my code:
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;
}
}
}
Now I can initialize a bank as:
Bank bank1 = new Bank("TestBank1", balance -> balance.compareTo(new BigDecimal("10000")) == -1 ? new BigDecimal("4") : new BigDecimal("5"));
While I can create another bank as:
Bank bank2 = new Bank("TestBank2", balance -> balance.compareTo(new BigDecimal("8000")) == -1 ? new BigDecimal("3.5") : new BigDecimal("5.3"));
Now both these banks are created in memory and will work perfectly as long as the application is running. But when I need to persist this for long term use I cannot directly store a variable of type Function<BigDecimal, BigDecimal> into the MySQL database.
Many people suggested stored procedures and that would have worked if it was only one logic, like balance -> balance.compareTo(new BigDecimal("10000")) == -1 ? new BigDecimal("4") : new BigDecimal("5") for every bank in the banks table, but this information will change every time.
Which means if I have 50 entries in my banks table I need to create 50 different stored procedures with 50 kinds of logics for every entry in my banks table, to keep updating the APY field as the balance changes. Might there be a better way?