บทนำ: ปัญหาของ Raw JDBC
เมื่อใช้ JDBC โดยตรง มี “ช่องว่าง” ระหว่าง Java objects กับ database tables:
java// ❌ ปัญหา: ซ้ำซ้อนและ error-prone
ResultSet rs = statement.executeQuery("SELECT * FROM users");
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
String email = rs.getString("email");
int age = rs.getInt("age");
// ← ต้อง manually convert ResultSet → User object
User user = new User(id, name, email, age);
users.add(user);
}
// ← ถ้า table มี 50 columns ต้องเขียนทั้งหมด!
// ← ถ้า column name เปลี่ยน ต้อง update code
// ← ถ้าลืม column ไหน bug จะซ่อน
Object Mapping = Automatic conversion ระหว่าง database rows กับ Java objects
Object Mapping Concept
ความสัมพันธ์: POJO ↔ Database Table
textJava World Database World
┌──────────────┐ ┌──────────────┐
│ User (POJO) │ │ users table │
├──────────────┤ ├──────────────┤
│ - id │ ←────→ │ id │
│ - name │ │ name │
│ - email │ │ email │
│ - age │ │ age │
└──────────────┘ └──────────────┘
Mapping:
- Object field → Table column
- Object instance → Table row
- Collection of objects → Set of rows
ตัวอย่างที่ 1: Manual Object Mapping (Foundation)
POJO Class
java/**
* POJO = Plain Old Java Object
* ไม่มี business logic ซับซ้อน เพียงเก็บข้อมูล
*/
public class User {
private int id;
private String name;
private String email;
private int age;
// Constructor (no-arg)
public User() {
}
// Constructor (with all fields)
public User(int id, String name, String email, int age) {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
}
// Getters and Setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override
public String toString() {
return String.format(
"User{id=%d, name='%s', email='%s', age=%d}",
id, name, email, age
);
}
}
Manual Mapping in DAO
javaimport java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class UserDAO {
/**
* Manual mapping: ResultSet → User object
*/
public List<User> getAllUsers() {
List<User> users = new ArrayList<>();
String sql = "SELECT id, name, email, age FROM users";
try (Connection conn = DatabaseConfig.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
// ← Manual mapping for each row
while (rs.next()) {
// Extract data from ResultSet
int id = rs.getInt("id");
String name = rs.getString("name");
String email = rs.getString("email");
int age = rs.getInt("age");
// Create User object
User user = new User(id, name, email, age);
users.add(user);
}
System.out.println("✓ Retrieved " + users.size() + " users");
} catch (SQLException e) {
System.err.println("❌ Error: " + e.getMessage());
}
return users;
}
/**
* Helper method: Map single ResultSet row to User
* ← Reusable mapping logic
*/
private User mapResultSetToUser(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
user.setAge(rs.getInt("age"));
return user;
}
/**
* Using helper method (Cleaner code)
*/
public User getUserById(int userId) {
String sql = "SELECT id, name, email, age FROM users WHERE id = ?";
try (Connection conn = DatabaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, userId);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
// ← Use helper method
return mapResultSetToUser(rs);
}
}
} catch (SQLException e) {
System.err.println("❌ Error: " + e.getMessage());
}
return null;
}
/**
* Reverse mapping: User object → INSERT statement
*/
public int createUser(User user) {
String sql = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
int generatedId = -1;
try (Connection conn = DatabaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(
sql,
Statement.RETURN_GENERATED_KEYS
)) {
// ← Map User object fields to SQL parameters
pstmt.setString(1, user.getName());
pstmt.setString(2, user.getEmail());
pstmt.setInt(3, user.getAge());
int rowsInserted = pstmt.executeUpdate();
if (rowsInserted > 0) {
try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
generatedId = generatedKeys.getInt(1);
}
}
}
} catch (SQLException e) {
System.err.println("❌ Error: " + e.getMessage());
}
return generatedId;
}
/**
* Reverse mapping: User object → UPDATE statement
*/
public boolean updateUser(User user) {
String sql = "UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?";
try (Connection conn = DatabaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
// ← Map User fields to UPDATE parameters
pstmt.setString(1, user.getName());
pstmt.setString(2, user.getEmail());
pstmt.setInt(3, user.getAge());
pstmt.setInt(4, user.getId());
int rowsUpdated = pstmt.executeUpdate();
return rowsUpdated > 0;
} catch (SQLException e) {
System.err.println("❌ Error: " + e.getMessage());
}
return false;
}
}
// ==== Usage ====
public class MappingExample {
public static void main(String[] args) {
UserDAO userDAO = new UserDAO();
// Get all users (ResultSet → User objects)
System.out.println("=== Retrieve and Map ===");
List<User> users = userDAO.getAllUsers();
users.forEach(System.out::println);
// Create user (User object → INSERT)
System.out.println("\n=== Create from Object ===");
User newUser = new User(0, "Alice", "[email protected]", 28);
int userId = userDAO.createUser(newUser);
System.out.println("Created user with ID: " + userId);
// Update user (User object → UPDATE)
System.out.println("\n=== Update from Object ===");
User toUpdate = new User(1, "John Updated", "[email protected]", 26);
boolean updated = userDAO.updateUser(toUpdate);
System.out.println("Update success: " + updated);
}
}
ตัวอย่างที่ 2: Mapping with Multiple Tables
Multiple POJO Classes
java// Product POJO
public class Product {
private int id;
private String name;
private double price;
private int stock;
public Product() {}
public Product(int id, String name, double price, int stock) {
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
}
// Getters and Setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getStock() { return stock; }
public void setStock(int stock) { this.stock = stock; }
@Override
public String toString() {
return String.format(
"Product{id=%d, name='%s', price=%.2f, stock=%d}",
id, name, price, stock
);
}
}
// Order POJO (with embedded objects)
public class Order {
private int id;
private User user; // ← Nested User object
private Product product; // ← Nested Product object
private int quantity;
private double totalPrice;
public Order() {}
public Order(int id, User user, Product product,
int quantity, double totalPrice) {
this.id = id;
this.user = user;
this.product = product;
this.quantity = quantity;
this.totalPrice = totalPrice;
}
// Getters and Setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public User getUser() { return user; }
public void setUser(User user) { this.user = user; }
public Product getProduct() { return product; }
public void setProduct(Product product) { this.product = product; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public double getTotalPrice() { return totalPrice; }
public void setTotalPrice(double totalPrice) { this.totalPrice = totalPrice; }
@Override
public String toString() {
return String.format(
"Order{id=%d, user=%s, product=%s, qty=%d, total=%.2f}",
id, user.getName(), product.getName(), quantity, totalPrice
);
}
}
DAO with JOINs (Complex Mapping)
javaimport java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class OrderDAO {
/**
* Get all orders with user and product details
* Requires JOIN mapping
*/
public List<Order> getAllOrders() {
List<Order> orders = new ArrayList<>();
// SQL with JOINs to get all related data
String sql = "SELECT " +
"o.id as order_id, o.quantity, o.total_price, " +
"u.id as user_id, u.name, u.email, u.age, " +
"p.id as product_id, p.name as product_name, p.price, p.stock " +
"FROM orders o " +
"JOIN users u ON o.user_id = u.id " +
"JOIN products p ON o.product_id = p.id";
try (Connection conn = DatabaseConfig.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
// Map User (from JOIN)
User user = new User(
rs.getInt("user_id"),
rs.getString("name"),
rs.getString("email"),
rs.getInt("age")
);
// Map Product (from JOIN)
Product product = new Product(
rs.getInt("product_id"),
rs.getString("product_name"),
rs.getDouble("price"),
rs.getInt("stock")
);
// Map Order (with nested objects)
Order order = new Order(
rs.getInt("order_id"),
user,
product,
rs.getInt("quantity"),
rs.getDouble("total_price")
);
orders.add(order);
}
System.out.println("✓ Retrieved " + orders.size() + " orders");
} catch (SQLException e) {
System.err.println("❌ Error: " + e.getMessage());
}
return orders;
}
/**
* Helper: Map single row from JOINed query
*/
private Order mapResultSetToOrder(ResultSet rs) throws SQLException {
// Map User
User user = new User(
rs.getInt("user_id"),
rs.getString("name"),
rs.getString("email"),
rs.getInt("age")
);
// Map Product
Product product = new Product(
rs.getInt("product_id"),
rs.getString("product_name"),
rs.getDouble("price"),
rs.getInt("stock")
);
// Map Order with nested objects
Order order = new Order(
rs.getInt("order_id"),
user,
product,
rs.getInt("quantity"),
rs.getDouble("total_price")
);
return order;
}
/**
* Get order by ID (with all details)
*/
public Order getOrderById(int orderId) {
String sql = "SELECT " +
"o.id as order_id, o.quantity, o.total_price, " +
"u.id as user_id, u.name, u.email, u.age, " +
"p.id as product_id, p.name as product_name, p.price, p.stock " +
"FROM orders o " +
"JOIN users u ON o.user_id = u.id " +
"JOIN products p ON o.product_id = p.id " +
"WHERE o.id = ?";
try (Connection conn = DatabaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, orderId);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return mapResultSetToOrder(rs);
}
}
} catch (SQLException e) {
System.err.println("❌ Error: " + e.getMessage());
}
return null;
}
/**
* Create order (Map Order object → INSERT)
*/
public int createOrder(Order order) {
String sql = "INSERT INTO orders (user_id, product_id, quantity, total_price) " +
"VALUES (?, ?, ?, ?)";
int generatedId = -1;
try (Connection conn = DatabaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
// Map Order object to INSERT parameters
pstmt.setInt(1, order.getUser().getId());
pstmt.setInt(2, order.getProduct().getId());
pstmt.setInt(3, order.getQuantity());
pstmt.setDouble(4, order.getTotalPrice());
int rowsInserted = pstmt.executeUpdate();
if (rowsInserted > 0) {
try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
generatedId = generatedKeys.getInt(1);
}
}
}
} catch (SQLException e) {
System.err.println("❌ Error: " + e.getMessage());
}
return generatedId;
}
}
// ==== Usage ====
public class ComplexMappingExample {
public static void main(String[] args) {
OrderDAO orderDAO = new OrderDAO();
// Get all orders (complex mapping with JOINs)
System.out.println("=== All Orders ===");
List<Order> allOrders = orderDAO.getAllOrders();
allOrders.forEach(System.out::println);
// Get order by ID
System.out.println("\n=== Get Order by ID ===");
Order order = orderDAO.getOrderById(1);
if (order != null) System.out.println(order);
// Create order (nested object mapping)
System.out.println("\n=== Create Order ===");
User user = new User(1, "John", "[email protected]", 25);
Product product = new Product(1, "Laptop", 999.99, 5);
Order newOrder = new Order(0, user, product, 1, 999.99);
int orderId = orderDAO.createOrder(newOrder);
System.out.println("Created order with ID: " + orderId);
}
}
ตัวอย่างที่ 3: Generic Mapping Pattern
Generic Helper Utility
javaimport java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
/**
* Generic mapper for JDBC ResultSet to objects
* Reduces code duplication
*/
public class ResultSetMapper {
/**
* Map single row to object
* @param rs ResultSet
* @param mapper Function to map row to object
* @return Mapped object
*/
public static <T> T mapToObject(ResultSet rs, Function<ResultSet, T> mapper)
throws SQLException {
return mapper.apply(rs);
}
/**
* Map all rows to list
* @param rs ResultSet
* @param mapper Function to map each row
* @return List of mapped objects
*/
public static <T> List<T> mapToList(ResultSet rs, Function<ResultSet, T> mapper)
throws SQLException {
List<T> list = new ArrayList<>();
while (rs.next()) {
list.add(mapper.apply(rs));
}
return list;
}
}
// ==== Usage with Lambda ====
public class GenericMappingExample {
public List<User> getAllUsers() {
String sql = "SELECT * FROM users";
try (Connection conn = DatabaseConfig.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
// ← Use generic mapper with lambda
return ResultSetMapper.mapToList(rs, resultSet -> {
try {
return new User(
resultSet.getInt("id"),
resultSet.getString("name"),
resultSet.getString("email"),
resultSet.getInt("age")
);
} catch (SQLException e) {
throw new RuntimeException(e);
}
});
} catch (SQLException e) {
System.err.println("❌ Error: " + e.getMessage());
}
return new ArrayList<>();
}
}
ตัวอย่างที่ 4: Mapping with Type Conversion
Handling Special Data Types
javaimport java.sql.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
public class Product {
private int id;
private String name;
private double price;
private int stock;
private LocalDate createdDate; // ← Date type
private boolean isActive; // ← Boolean type
public Product() {}
public Product(int id, String name, double price, int stock,
LocalDate createdDate, boolean isActive) {
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
this.createdDate = createdDate;
this.isActive = isActive;
}
// Getters and Setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getStock() { return stock; }
public void setStock(int stock) { this.stock = stock; }
public LocalDate getCreatedDate() { return createdDate; }
public void setCreatedDate(LocalDate createdDate) { this.createdDate = createdDate; }
public boolean isActive() { return isActive; }
public void setActive(boolean active) { isActive = active; }
}
public class ProductDAO {
/**
* Map with type conversion
*/
public List<Product> getAllProducts() {
List<Product> products = new ArrayList<>();
String sql = "SELECT id, name, price, stock, created_date, is_active FROM products";
try (Connection conn = DatabaseConfig.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
Product product = new Product();
// Standard types
product.setId(rs.getInt("id"));
product.setName(rs.getString("name"));
product.setPrice(rs.getDouble("price"));
product.setStock(rs.getInt("stock"));
// ← Type conversion: SQL DATE → Java LocalDate
product.setCreatedDate(
rs.getDate("created_date").toLocalDate()
);
// ← Type conversion: SQL Boolean → Java boolean
product.setActive(rs.getBoolean("is_active"));
products.add(product);
}
} catch (SQLException e) {
System.err.println("❌ Error: " + e.getMessage());
}
return products;
}
/**
* Reverse mapping with type conversion
*/
public int createProduct(Product product) {
String sql = "INSERT INTO products (name, price, stock, created_date, is_active) " +
"VALUES (?, ?, ?, ?, ?)";
int generatedId = -1;
try (Connection conn = DatabaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(1, product.getName());
pstmt.setDouble(2, product.getPrice());
pstmt.setInt(3, product.getStock());
// ← Type conversion: Java LocalDate → SQL Date
pstmt.setDate(4, java.sql.Date.valueOf(product.getCreatedDate()));
// ← Type conversion: Java boolean → SQL Boolean
pstmt.setBoolean(5, product.isActive());
int rowsInserted = pstmt.executeUpdate();
if (rowsInserted > 0) {
try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
generatedId = generatedKeys.getInt(1);
}
}
}
} catch (SQLException e) {
System.err.println("❌ Error: " + e.getMessage());
}
return generatedId;
}
}
// ==== Usage ====
public class TypeConversionExample {
public static void main(String[] args) {
ProductDAO productDAO = new ProductDAO();
// Get all products (with type conversion)
System.out.println("=== All Products ===");
List<Product> products = productDAO.getAllProducts();
products.forEach(p -> System.out.println(
p.getName() + " - $" + p.getPrice() +
" (Created: " + p.getCreatedDate() + ")"
));
// Create product (reverse mapping with type conversion)
System.out.println("\n=== Create Product ===");
Product newProduct = new Product(
0,
"New Product",
29.99,
100,
LocalDate.now(),
true
);
int productId = productDAO.createProduct(newProduct);
System.out.println("Created product with ID: " + productId);
}
}
Best Practices: Object Mapping Checklist
text✓ DO:
☑ Create POJO for each table
☑ Use getters/setters for fields
☑ Extract mapping logic to helper methods
☑ Handle NULL values properly
☑ Convert data types correctly
☑ Use consistent naming (field ↔ column)
☑ Document mapping in comments
☑ Test mapping with edge cases
✗ DON'T:
☐ Map directly in business logic
☐ Use public fields instead of properties
☐ Forget NULL checks
☐ Assume column names match field names
☐ Ignore type conversion (SQL ↔ Java)
☐ Create separate classes for each query
☐ Map columns that don't exist
Advanced: Annotation-Based Mapping (Preview)
Concept สำหรับอนาคต
java// Note: This is a PREVIEW of advanced mapping
// You'll learn more about annotations in future courses
/**
* Hypothetical annotation-based mapping
* (Similar to JPA/Hibernate in real projects)
*/
@Entity(tableName = "users")
public class UserAnnotated {
@Column(name = "id")
private int id;
@Column(name = "name")
private String name;
@Column(name = "email")
private String email;
@Column(name = "age")
private int age;
// In production frameworks, mapping is automatic!
// ← ข้อมูลนี้จะ learn ในหน่วยเรียนเกี่ยวกับ Frameworks
}
// With annotations, you don't need to write manual mapping code!
// Frameworks handle it automatically
สรุป
Object Mapping (POJO ↔ Table) ไม่ใช่แค่ “convert data” แต่เป็นการออกแบบ bridge ระหว่าง 2 worlds:
ทำไมต้อง Object Mapping:
- Encapsulation – Wrap database data ใน objects
- Reusability – Same POJO ใช้ได้หลายที่
- Maintainability – Change mapping ใน 1 ที่เท่านั้น
- Type Safety – Use objects instead of raw ResultSet
3 Levels of Mapping:
- Manual – Write conversion code by hand (foundation)
- Helper Methods – Extract common patterns (DRY)
- Generic Utilities – Use lambdas/functions (scalable)
สิ่งที่ต้องจำ:
- POJO = Plain Java Object ไม่มี DB knowledge
- Mapping ใน DAO layer เท่านั้น (separation of concerns)
- Handle type conversion (SQL Date ↔ Java LocalDate)
- Test edge cases (NULL values, type mismatches)
เมื่อ master Object Mapping:
- Code ของคุณ cleaner – ไม่มี raw SQL conversions
- Code ของคุณ more organized – clear responsibilities
- Code ของคุณ easier to maintain – centralized mapping logic
- Transition ไปสู่ ORM frameworks (JPA/Hibernate) จะง่าย
Object Mapping คือ “contract” ระหว่าง database world กับ Java world – มันทำให้ทั้ง 2 sides สามารถพูดคุยกันได้อย่างมีระบบและปลอดภัย
