Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Building a Minimalistic MyBatis Implementation

Tech May 8 6

Introduction

MyBatis serves as a persistence framework that simplifies database operations by eliminating repetitive tasks associated with traditional JDBC usage. It allows developers to focus more on business logic rather than boilerplate code, offering flexible SQL construction and result mapping capabilities.

This article walks through building a simplified version of MyBatis that demonstrates core features including:

  • Streamlined database interactions
  • Flexible SQL statement handling
  • Parameter binding
  • Result set processing

JDBC Overview

As MyBatis is built upon JDBC, we begin by examining standard JDBC practices before applying encapsulation techniques.

Entity Class

public class User {
    private Integer id;
    private String name;
    private Integer age;

    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Standard JDBC Connection

public class DatabaseConnector {
    private static final String URL = "jdbc:mysql://localhost:3306/test";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "root";

    public static Connection getConnection() throws SQLException {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            throw new SQLException("Driver not found", e);
        }
        return DriverManager.getConnection(URL, USERNAME, PASSWORD);
    }
}

Manual JDBC Query Example

Connection conn = DatabaseConnector.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM user WHERE name = ?");
stmt.setString(1, "Zhang San");
ResultSet rs = stmt.executeQuery();
List<User> users = new ArrayList<>();
while(rs.next()) {
    User user = new User();
    user.setAge(rs.getInt("age"));
    user.setId(rs.getInt("id"));
    user.setName(rs.getString("name"));
    users.add(user);
}
System.out.println(users);

Core Implementation

We now proceed to implement a simplified MyBatis-like mechanism using proxy-based invocation.

Mapper Interface

public interface UserMapper {
    @Select("SELECT * FROM user WHERE name = #{name} OR age = #{age}")
    List<User> findAllUsers(@Param("name") String name, @Param("age") Integer age);

    @Select("SELECT * FROM user WHERE name = #{name}")
    User findUser(@Param("name") String name);
}

Annotation Definitions

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Param {
    String value();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Select {
    String value();
}

Proxy Factory

public class MapperProxyFactory {
    private static final Map<Class<?>, TypeHandler<?>> TYPE_HANDLER_MAP = new HashMap<>();

    static {
        TYPE_HANDLER_MAP.put(Integer.class, new IntegerTypeHandler());
        TYPE_HANDLER_MAP.put(String.class, new StringTypeHandler());
    }

    public static <T> T getMapper(Class<T> mapperInterface) {
        return (T) Proxy.newProxyInstance(
            ClassLoader.getSystemClassLoader(),
            new Class[]{mapperInterface},
            (proxy, method, args) -> {
                Select selectAnnotation = method.getAnnotation(Select.class);
                String sql = selectAnnotation.value();

                ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
                GenericTokenParser parser = new GenericTokenParser("#{", "}", tokenHandler);
                String parsedSql = parser.parse(sql);

                Connection conn = DatabaseConnector.getConnection();
                PreparedStatement stmt = conn.prepareStatement(parsedSql);

                Map<String, Object> paramMap = new HashMap<>();
                Parameter[] params = method.getParameters();
                for (int i = 0; i < params.length; i++) {
                    Param param = params[i].getAnnotation(Param.class);
                    String key = param.value();
                    paramMap.put(key, args[i]);
                    paramMap.put(params[i].getName(), args[i]);
                }

                List<ParameterMapping> mappings = tokenHandler.getParameterMappings();
                for (int i = 0; i < mappings.size(); i++) {
                    String property = mappings.get(i).getProperty();
                    Object value = paramMap.get(property);
                    Class<?> valueType = value.getClass();
                    TYPE_HANDLER_MAP.get(valueType).setParameter(stmt, i + 1, value);
                }

                stmt.execute();
                ResultSet rs = stmt.getResultSet();

                List<Object> results = new ArrayList<>();
                Type returnType = method.getGenericReturnType();
                Class<?> resultType;
                boolean isCollection = false;

                if (returnType instanceof Class<?>) {
                    resultType = (Class<?>) returnType;
                } else if (returnType instanceof ParameterizedType) {
                    ParameterizedType pType = (ParameterizedType) returnType;
                    resultType = (Class<?>) pType.getActualTypeArguments()[0];
                    isCollection = true;
                } else {
                    throw new UnsupportedOperationException("Unsupported return type");
                }

                ResultSetMetaData metaData = rs.getMetaData();
                List<String> columns = new ArrayList<>();
                for (int i = 1; i <= metaData.getColumnCount(); i++) {
                    columns.add(metaData.getColumnName(i));
                }

                Method[] setters = resultType.getMethods();
                Map<String, Method> setterMap = new HashMap<>();
                for (Method m : setters) {
                    if (m.getName().startsWith("set")) {
                        String prop = m.getName().substring(3);
                        String lowerProp = Character.toLowerCase(prop.charAt(0)) + prop.substring(1);
                        setterMap.put(lowerProp, m);
                    }
                }

                while (rs.next()) {
                    Object instance = resultType.newInstance();
                    for (String col : columns) {
                        Method setter = setterMap.get(col);
                        if (setter != null) {
                            Class<?> argType = setter.getParameterTypes()[0];
                            TypeHandler<?> handler = TYPE_HANDLER_MAP.get(argType);
                            Object val = handler.getResult(rs, col);
                            setter.invoke(instance, val);
                        }
                    }
                    results.add(instance);
                }

                return isCollection ? results : results.isEmpty() ? null : results.get(0);
            }
        );
    }
}

SQL Parsing Component

public class GenericTokenParser {
    private final String openToken;
    private final String closeToken;
    private final TokenHandler handler;

    public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
        this.openToken = openToken;
        this.closeToken = closeToken;
        this.handler = handler;
    }

    public String parse(String text) {
        if (text == null || text.isEmpty()) return "";
        int start = text.indexOf(openToken);
        if (start == -1) return text;

        char[] src = text.toCharArray();
        int offset = 0;
        StringBuilder builder = new StringBuilder();
        StringBuilder expression = null;

        while (start > -1) {
            if (start > 0 && src[start - 1] == '\\') {
                builder.append(src, offset, start - offset - 1).append(openToken);
                offset = start + openToken.length();
            } else {
                if (expression == null) expression = new StringBuilder();
                else expression.setLength(0);

                builder.append(src, offset, start - offset);
                offset = start + openToken.length();

                int end = text.indexOf(closeToken, offset);
                while (end > -1 && src[end - 1] == '\\') {
                    expression.append(src, offset, end - offset - 1).append(closeToken);
                    offset = end + closeToken.length();
                    end = text.indexOf(closeToken, offset);
                }

                if (end == -1) {
                    builder.append(src, start, src.length - start);
                    offset = src.length;
                } else {
                    expression.append(src, offset, end - offset);
                    builder.append(handler.handleToken(expression.toString()));
                    offset = end + closeToken.length();
                }
            }
            start = text.indexOf(openToken, offset);
        }

        if (offset < src.length) {
            builder.append(src, offset, src.length - offset);
        }
        return builder.toString();
    }
}

Parameter Mapping

public class ParameterMapping {
    private final String property;

    public ParameterMapping(String property) {
        this.property = property;
    }

    public String getProperty() {
        return property;
    }
}

Token Handler for Parameters

public class ParameterMappingTokenHandler implements TokenHandler {
    private final List<ParameterMapping> parameterMappings = new ArrayList<>();

    @Override
    public String handleToken(String content) {
        parameterMappings.add(new ParameterMapping(content));
        return "?";
    }

    public List<ParameterMapping> getParameterMappings() {
        return parameterMappings;
    }
}

public interface TokenHandler {
    String handleToken(String content);
}

Type Handlers

public interface TypeHandler<T> {
    void setParameter(PreparedStatement ps, Integer index, T value) throws SQLException;
    T getResult(ResultSet rs, String columnName) throws SQLException;
}

public class StringTypeHandler implements TypeHandler<String> {
    @Override
    public void setParameter(PreparedStatement ps, Integer index, String value) throws SQLException {
        ps.setString(index, value);
    }

    @Override
    public String getResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getString(columnName);
    }
}

public class IntegerTypeHandler implements TypeHandler<Integer> {
    @Override
    public void setParameter(PreparedStatement ps, Integer index, Integer value) throws SQLException {
        ps.setInt(index, value);
    }

    @Override
    public Integer getResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getInt(columnName);
    }
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.