将 JDBC ResultSet 映射到对象

Posted

技术标签:

【中文标题】将 JDBC ResultSet 映射到对象【英文标题】:Mapping a JDBC ResultSet to an object 【发布时间】:2014-03-24 05:50:22 【问题描述】:

我有一个用户类,它有 16 个属性,例如名字、姓氏、dob、用户名、密码等……这些都存储在 mysql 数据库中,当我想检索用户时,我使用 ResultSet。我想将每一列映射回用户属性,但我这样做的方式似乎非常低效。 比如我在做:

//ResultSet rs;
while(rs.next()) 
   String uid = rs.getString("UserId");
   String fname = rs.getString("FirstName");
   ...
   ...
   ...
   User u = new User(uid,fname,...);
   //ArrayList<User> users 
   users.add(u);
 

即我检索所有列,然后通过将所有列值插入用户构造函数来创建用户对象。

有没有人知道一种更快、更整洁的方法?

【问题讨论】:

你的意思。有效吗?是不是太费时间了 查看 Spring JDBC 模板及其 bean 映射器 有很多工具可以让这种任务变得更容易。我认为最好的是 sql2o、JDBI 和 jOOQ Link 【参考方案1】:

有推荐使用https://commons.apache.org/proper/commons-dbutils/的答案。行处理器的默认实现,即 db-utils 1.7 中的 org.apache.commons.dbutils.BasicRowProcessor 不是线程安全的。因此,如果您在多线程环境中使用org.apache.commons.dbutils.QueryRunner::query 方法,您应该编写自定义行处理器。它可以通过实现org.apache.commons.dbutils.RowProcessor 接口或扩展org.apache.commons.dbutils.BasicRowProcessor 类来完成。下面通过扩展BasicRowProcessor给出示例代码:

class PersonResultSetHandler extends BasicRowProcessor 
    @Override
    public <T> List<T> toBeanList(ResultSet rs, Class<? extends T> type) 
    throws SQLException 
   
     //Handle the ResultSet and return a List of Person 
     List<Person> personList = ..... 
     return (List<T>) personList;
   
        

将自定义行处理器传递给适当的org.apache.commons.dbutils.ResultSetHandler 实现。以下代码中使用了BeanListHandler

QueryRunner qr = new QueryRunner();
List<Person> personList = qr.query(conn, sqlQuery, new BeanListHandler<Person>(Person.class, new PersonResultSetHandler()));                                                                                                                                                 

但是,https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc 是另一种具有更简洁 API 的替代方案。虽然,我不确定它的线程安全方面。

【讨论】:

【参考方案2】:

如果您不想使用任何 JPA 提供程序,例如 OpenJPA 或 Hibernate,您可以试试 Apache DbUtils。

http://commons.apache.org/proper/commons-dbutils/examples.html

那么您的代码将如下所示:

QueryRunner run = new QueryRunner(dataSource);

// Use the BeanListHandler implementation to convert all
// ResultSet rows into a List of Person JavaBeans.
ResultSetHandler<List<Person>> h = new BeanListHandler<Person>(Person.class);

// Execute the SQL statement and return the results in a List of
// Person objects generated by the BeanListHandler.
List<Person> persons = run.query("SELECT * FROM Person", h);

【讨论】:

对于没有任何框架的纯 jdbc 逻辑来说似乎是完美的解决方案。凉爽的 ! +1 DbUtils 在很多方面看起来都很好,但它有两个缺点:它依赖于 java.desktop 模块,并且它对将 bean 状态存储到 @ 语句中的任务没有帮助987654324@ 声明。【参考方案3】:

我想暗示一下 q2o。它是一个基于 JPA 的 Java 对象映射器,可帮助完成许多繁琐的 SQL 和 JDBC ResultSet 相关任务,但没有 ORM 框架所带来的所有复杂性。在它的帮助下,将 ResultSet 映射到对象就像这样简单:

while(rs.next()) 
    users.add(Q2Obj.fromResultSet(rs, User.class));

More about q2o can be found here.

【讨论】:

【参考方案4】:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.json.simple.JSONObject;
import com.google.gson.Gson;

public class ObjectMapper 

//generic method to convert JDBC resultSet into respective DTo class
@SuppressWarnings("unchecked")
public static Object mapValue(List<Map<String, Object>> rows,Class<?> className) throws Exception


        List<Object> response=new ArrayList<>(); 
        Gson gson=new Gson();

        for(Map<String, Object> row:rows)
        org.json.simple.JSONObject jsonObject = new JSONObject();
        jsonObject.putAll(row);
        String json=jsonObject.toJSONString();
        Object actualObject=gson.fromJson(json, className);
        response.add(actualObject);
        
        return response;

    

    public static void main(String args[]) throws Exception

        List<Map<String, Object>> rows=new ArrayList<Map<String, Object>>(); 

        //Hardcoded data for testing
        Map<String, Object> row1=new HashMap<String, Object>();
        row1.put("name", "Raja");
        row1.put("age", 22);
        row1.put("location", "India");


        Map<String, Object> row2=new HashMap<String, Object>();
        row2.put("name", "Rani");
        row2.put("age", 20);
        row2.put("location", "India");

        rows.add(row1);
        rows.add(row2);


        @SuppressWarnings("unchecked")
        List<Dto> res=(List<Dto>) mapValue(rows, Dto.class);


    

    

    public class Dto 

    private String name;
    private Integer age;
    private String location;

    //getters and setters

    

试试上面的代码。这可以用作将 JDBC 结果映射到相应 DTO 类的通用方法。

【讨论】:

【参考方案5】:

使用 DbUtils...

我对那个库的唯一问题是有时你的 bean 类中有关系,DBUtils 不会映射它。它只映射 bean 类中的属性,如果您有其他复杂的属性(由于 DB 关系而引用其他 bean),您必须创建我所说的“间接设置器”,它们是将值放入那些复杂的设置器属性的属性。

【讨论】:

【参考方案6】:

使用@TEH-EMPRAH 想法和来自Cast Object to Generic Type for returning 的通用转换的完整解决方案

import annotations.Column;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.*;

public class ObjectMapper<T> 

    private Class clazz;
    private Map<String, Field> fields = new HashMap<>();
    Map<String, String> errors = new HashMap<>();

    public DataMapper(Class clazz) 
        this.clazz = clazz;

        List<Field> fieldList = Arrays.asList(clazz.getDeclaredFields());
        for (Field field : fieldList) 
            Column col = field.getAnnotation(Column.class);
            if (col != null) 
                field.setAccessible(true);
                fields.put(col.name(), field);
            
        
    

    public T map(Map<String, Object> row) throws SQLException 
        try 
            T dto = (T) clazz.getConstructor().newInstance();
            for (Map.Entry<String, Object> entity : row.entrySet()) 
                if (entity.getValue() == null) 
                    continue;  // Don't set DBNULL
                
                String column = entity.getKey();
                Field field = fields.get(column);
                if (field != null) 
                    field.set(dto, convertInstanceOfObject(entity.getValue()));
                
            
            return dto;
         catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) 
            e.printStackTrace();
            throw new SQLException("Problem with data Mapping. See logs.");
        
    

    public List<T> map(List<Map<String, Object>> rows) throws SQLException 
        List<T> list = new LinkedList<>();

        for (Map<String, Object> row : rows) 
            list.add(map(row));
        

        return list;
    

    private T convertInstanceOfObject(Object o) 
        try 
            return (T) o;
         catch (ClassCastException e) 
            return null;
        
    

然后就它与数据库的关系而言,我有以下几点:

// connect to database (autocloses)
try (DataConnection conn = ds1.getConnection()) 

    // fetch rows
    List<Map<String, Object>> rows = conn.nativeSelect("SELECT * FROM products");

    // map rows to class
    ObjectMapper<Product> objectMapper = new ObjectMapper<>(Product.class);
    List<Product> products = objectMapper.map(rows);

    // display the rows
    System.out.println(rows);

    // display it as products
    for (Product prod : products) 
        System.out.println(prod);
    

 catch (Exception e) 
    e.printStackTrace();

【讨论】:

代码convertInstanceOfObject 永远不会抛出ClassCastException,所以没有必要去捕捉它。演员表是未经检查的演员表。相反,如果类型不匹配,则在调用代码将返回值分配给变量时将抛出ClassCastException【参考方案7】:

假设您想使用核心 Java,而没有任何战略框架。如果可以保证,实体的字段名称将等于数据库中的列,则可以使用反射 API(否则创建注释并在此处定义映射名称)

按字段名

/**

Class<T> clazz - a list of object types you want to be fetched
ResultSet resultSet - pointer to your retrieved results 

*/

    List<Field> fields = Arrays.asList(clazz.getDeclaredFields());
    for(Field field: fields) 
        field.setAccessible(true);
    

    List<T> list = new ArrayList<>(); 
    while(resultSet.next()) 

        T dto = clazz.getConstructor().newInstance();

        for(Field field: fields) 
            String name = field.getName();

            try
                String value = resultSet.getString(name);
                field.set(dto, field.getType().getConstructor(String.class).newInstance(value));
             catch (Exception e) 
                e.printStackTrace();
            

        

        list.add(dto);

    

通过注释

@Retention(RetentionPolicy.RUNTIME)
public @interface Col 

    String name();


DTO:

class SomeClass 

   @Col(name = "column_in_db_name")
   private String columnInDbName;

   public SomeClass() 

   // ..



相同,但是

    while(resultSet.next()) 

        T dto = clazz.getConstructor().newInstance();

        for(Field field: fields) 
            Col col = field.getAnnotation(Col.class);
            if(col!=null) 
                String name = col.name();
                try
                    String value = resultSet.getString(name);
                    field.set(dto, field.getType().getConstructor(String.class).newInstance(value));
                 catch (Exception e) 
                    e.printStackTrace();
                
            
        

        list.add(dto);

    

想法

事实上,遍历所有字段似乎无效,所以我会将映射存储在某个地方,而不是每次都进行迭代。但是,如果我们的T 是一个仅用于传输数据并且不包含大量不必要字段的 DTO,那没关系。最后,它比一直使用样板方法要好得多。

希望这对某人有所帮助。

【讨论】:

该模式与我自己的情况非常相似。只是我在这个模式上的美分。 a)您需要检查您是否可以访问该字段(我通过设置始终可读的字段来覆盖该字段 b)您可以首先开始检查您的类,该类可能 95% 的字段少于您从 db 获得的字段。由于许多原因,一个类不需要一切 c) 快速迭代以检查相等性仅通过 null 和 equals() 可行 d) 我最近放置了一个 CacheManager 构建阅读教程并显着提高了反射的性能 e) 你需要缓存可重复查询结果。【参考方案8】:

如果您要检索更多记录,请使用 Statement Fetch Size 。像这样。

Statement statement = connection.createStatement();
statement.setFetchSize(1000); 

除此之外,我认为您在性能方面的做法没有问题

就整洁而言。始终使用单独的方法委托将结果集映射到 POJO 对象。以后可以在同一个类中重复使用

喜欢

private User mapResultSet(ResultSet rs)
     User user = new User();
     // Map Results
     return user;

如果 columnName 和 object 的 fieldName 的名称相同,您还可以编写反射实用程序将记录加载回 POJO。并使用 MetaData 读取 columnNames 。但是对于使用反射的小规模项目来说不是问题。但正如我之前所说,你的做法没有任何问题。

【讨论】:

【参考方案9】:

无需将 resultSet 值存储到 String 并再次设置到 POJO 类中。而是在您检索时设置。

或者最好的方法是切换到像 hibernate 这样的 ORM 工具,而不是 JDBC,它将你的 POJO 对象直接映射到数据库。

但是现在使用这个:

List<User> users=new ArrayList<User>();

while(rs.next()) 
   User user = new User();      
   user.setUserId(rs.getString("UserId"));
   user.setFName(rs.getString("FirstName"));
  ...
  ...
  ...


  users.add(user);
 

【讨论】:

是的,这样会更简洁更好,因为我不必不必要地创建所有字符串、整数等(也不使用构造函数)。谢谢。 如果结果集包含 300 个字段怎么办? 如果将来添加一些列怎么办?然后,这里也需要进行更改。

以上是关于将 JDBC ResultSet 映射到对象的主要内容,如果未能解决你的问题,请参考以下文章

JDBC数据类型

Mybatis 处理列名—字段名映射 :驼峰式命名映射

Java中,将ResultSet映射为对象和队列及其他辅助函数

JDBC的ResultSet游标转spark的DataFrame,数据类型的映射以TeraData数据库为例

DAO 层可以返回一个 JDBC ResultSet (Java)

将 JSON 对象映射到 TypeScript 对象