If you've worked on a plain JDBC application with 10+ domain objects and 50+ fields each, you know the maintenance pain: rename a column and you're hunting through controllers, models and view layers fixing hardcoded strings. There's a cleaner approach using two underused Java features โ ResultSetMetaData and Reflection.
The Problem
A typical JDBC MVC project with significant data complexity leads to:
- Column names hardcoded as strings in SQL queries, result set reads, and form field names
- Any schema change requiring updates across all three layers
- Tedious boilerplate in every DAO:
rs.getString("first_name"),rs.getLong("customer_id")โฆ
On a project with 50 fields per object across 10 objects, this becomes a synchronisation nightmare. One missed rename and you have a silent runtime bug.
The Solution: Let the Schema Drive the Mapping
Two Java features make this possible:
- ResultSetMetaData โ gives you column names, types and count dynamically at runtime
- Java Reflection โ lets you invoke setters by name without knowing them at compile time
Step 1 โ Read column names dynamically
ResultSet rs = stmt.executeQuery("SELECT * FROM customers");
ResultSetMetaData meta = rs.getMetaData();
int columnCount = meta.getColumnCount();
List<String> columns = new ArrayList<>();
for (int i = 1; i <= columnCount; i++) {
columns.add(meta.getColumnName(i));
}
Step 2 โ Map to object using Reflection
Customer customer = new Customer();
Class<?> clazz = customer.getClass();
while (rs.next()) {
for (String col : columns) {
// Convert "first_name" โ "setFirstName"
String setter = "set" + Character.toUpperCase(col.charAt(0))
+ col.substring(1).replaceAll("_([a-z])", m -> m.group(1).toUpperCase());
try {
Method method = clazz.getMethod(setter, String.class);
method.invoke(customer, rs.getString(col));
} catch (NoSuchMethodException e) {
// column has no matching setter โ skip gracefully
}
}
}
Step 3 โ Build SQL dynamically from schema
// Generic INSERT โ no hardcoded column names
StringBuilder sql = new StringBuilder("INSERT INTO " + tableName + " (");
StringBuilder values = new StringBuilder(" VALUES (");
for (int i = 1; i <= columnCount; i++) {
sql.append(meta.getColumnName(i));
values.append("?");
if (i < columnCount) { sql.append(", "); values.append(", "); }
}
sql.append(")").append(values).append(")");
Step 4 โ View layer: gather all form fields generically
// Angular: send all form fields without hardcoding names
$scope.submit = function() {
var payload = {};
angular.forEach($scope.formFields, function(field) {
payload[field.name] = field.value;
});
$http.post('/api/customer', payload);
};
The Payoff
With this pattern in place, adding a new column to a table means:
- Add the column to the database
- Add the corresponding getter/setter to the domain object
- Done โ the view, controller and DAO pick it up automatically
Note: if you're using Spring, Spring Data JPA or MyBatis, these frameworks already do this for you. This pattern is most valuable in legacy JDBC-heavy codebases where introducing a full ORM isn't feasible.