Object mapping (POJO <-> Table)

บทนำ: ปัญหาของ 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:

  1. Manual – Write conversion code by hand (foundation)
  2. Helper Methods – Extract common patterns (DRY)
  3. 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 สามารถพูดคุยกันได้อย่างมีระบบและปลอดภัย