【问题标题】:Mapping a JDBC ResultSet to an object将 JDBC ResultSet 映射到对象
【发布时间】: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

标签: java mysql object jdbc resultset


【解决方案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 个字段怎么办?
            • 如果将来添加一些列怎么办?然后,这里也需要进行更改。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2017-06-28
            • 1970-01-01
            • 2015-03-29
            • 1970-01-01
            • 2019-05-27
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多