JDBCTemplate устанавливает вложенный POJO с помощью BeanPropertyRowMapper

Учитывая следующий пример POJO: (Предположим, что Getters и Setters для всех свойств)

class User {
    String user_name;
    String display_name;
}

class Message {
    String title;
    String question;
    User user;
}

Можно легко запросить базу данных (postgres в моем случае) и заполнить список классов сообщений с помощью BeanPropertyRowMapper, где поле db сопоставило свойство в POJO: (Предположим, что таблицы DB имеют соответствующие поля для свойств POJO).

NamedParameterDatbase.query("SELECT * FROM message", new BeanPropertyRowMapper(Message.class));

Мне интересно - есть ли удобный способ построить один запрос и/или создать сопоставление строк таким образом, чтобы также заполнить свойства внутреннего "пользовательского" POJO в сообщении.

То есть, некоторые синтаксические магии, где каждая строка результата в запросе:

SELECT * FROM message, user WHERE user_id = message_id

Создайте список сообщений с ассоциированным пользователем


Случай использования:

В конечном итоге классы передаются обратно как сериализованный объект из контроллера Spring, классы вложены так, что полученный JSON/XML имеет приличную структуру.

В настоящий момент эта ситуация разрешается путем выполнения двух запросов и вручную настраивает пользовательское свойство каждого сообщения в цикле. Полезно, но я думаю, что более элегантный способ должен быть возможен.


Обновление: Используемое решение -

Престижность @Will Keeling для вдохновения для ответа с использованием настраиваемого сопоставления строк. Мое решение добавляет дополнение карт свойств bean, чтобы автоматизировать назначения полей.

Предостережение структурирует запрос так, чтобы соответствующие имена таблиц были префиксными (однако для этого не существует стандартного соглашения, поэтому запрос создается программно):

SELECT title AS "message.title", question AS "message.question", user_name AS "user.user_name", display_name AS "user.display_name" FROM message, user WHERE user_id = message_id

Затем настраиваемый сопоставитель строк создает несколько карт bean и устанавливает их свойства на основе префикса столбца: (используя метаданные для получения имени столбца).

public Object mapRow(ResultSet rs, int i) throws SQLException {

    HashMap<String, BeanMap> beans_by_name = new HashMap();

    beans_by_name.put("message", BeanMap.create(new Message()));
    beans_by_name.put("user", BeanMap.create(new User()));

    ResultSetMetaData resultSetMetaData = rs.getMetaData();

    for (int colnum = 1; colnum <= resultSetMetaData.getColumnCount(); colnum++) {

        String table = resultSetMetaData.getColumnName(colnum).split("\\.")[0];
        String field = resultSetMetaData.getColumnName(colnum).split("\\.")[1];

        BeanMap beanMap = beans_by_name.get(table);

        if (rs.getObject(colnum) != null) {
            beanMap.put(field, rs.getObject(colnum));
        }
    }

    Message m = (Task)beans_by_name.get("message").getBean();
    m.setUser((User)beans_by_name.get("user").getBean());

    return m;
}

Опять же, это может показаться излишним для объединения двух классов, но пример использования IRL включает несколько таблиц с десятками полей.

Ответ 1

Spring ввел новый AutoGrowNestedPaths свойство в интерфейс BeanMapper.

Пока SQL-запрос форматирует имена столбцов с помощью. разделитель (как и раньше), то преобразователь строк автоматически будет нацелен на внутренние объекты.

При этом я создал новый родоразрядчик строк следующим образом:

QUERY

SELECT title AS "message.title", question AS "message.question", user_name AS "user.user_name", display_name AS "user.display_name" FROM message, user WHERE user_id = message_id

ROW MAPPER:

package nested_row_mapper;

import org.springframework.beans.*;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.JdbcUtils;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;

public class NestedRowMapper<T> implements RowMapper<T> {

  private Class<T> mappedClass;

  public NestedRowMapper(Class<T> mappedClass) {
    this.mappedClass = mappedClass;
  }

  @Override
  public T mapRow(ResultSet rs, int rowNum) throws SQLException {

    T mappedObject = BeanUtils.instantiate(this.mappedClass);
    BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject);

    bw.setAutoGrowNestedPaths(true);

    ResultSetMetaData meta_data = rs.getMetaData();
    int columnCount = meta_data.getColumnCount();

    for (int index = 1; index <= columnCount; index++) {

      try {

        String column = JdbcUtils.lookupColumnName(meta_data, index);
        Object value = JdbcUtils.getResultSetValue(rs, index, Class.forName(meta_data.getColumnClassName(index)));

        bw.setPropertyValue(column, value);

      } catch (TypeMismatchException | NotWritablePropertyException | ClassNotFoundException e) {
         // Ignore
      }
    }

    return mappedObject;
  }
}

Ответ 2

Возможно, вы могли бы передать пользовательский RowMapper, который мог бы сопоставить каждую строку запроса объединенного соединения (между сообщением и пользователем) с Message и вложенным User. Что-то вроде этого:

List<Message> messages = jdbcTemplate.query("SELECT * FROM message m, user u WHERE u.message_id = m.message_id", new RowMapper<Message>() {
    @Override
    public Message mapRow(ResultSet rs, int rowNum) throws SQLException {
        Message message = new Message();
        message.setTitle(rs.getString(1));
        message.setQuestion(rs.getString(2));

        User user = new User();
        user.setUserName(rs.getString(3));
        user.setDisplayName(rs.getString(4));

        message.setUser(user);

        return message;
    }
});

Ответ 3

Обновление: 10/4/2015. Я обычно больше не делаю этого преобразования строк. Вы можете выполнить выборочное представление JSON более элегантно с помощью аннотаций. Смотрите gist.


Я потратил большую часть полного дня, пытаясь понять это для моего случая трехслойных вложенных объектов и просто окончательно прибил его. Вот моя ситуация:

Учетные записи (то есть пользователи) --1tomany → Роли --1tomany → виды (пользователю разрешено видеть)

(Эти классы POJO вставлены в самое нижнее.)

И я хотел, чтобы контроллер возвращал такой объект:

[ {
  "id" : 3,
  "email" : "[email protected]",
  "password" : "sdclpass",
  "org" : "Super-duper Candy Lab",
  "role" : {
    "id" : 2,
    "name" : "ADMIN",
    "views" : [ "viewPublicReports", "viewAllOrders", "viewProducts", "orderProducts", "viewOfferings", "viewMyData", "viewAllData", "home", "viewMyOrders", "manageUsers" ]
  }
}, {
  "id" : 5,
  "email" : "[email protected]",
  "password" : "stereopass",
  "org" : "Stereolab",
  "role" : {
    "id" : 1,
    "name" : "USER",
    "views" : [ "viewPublicReports", "viewProducts", "orderProducts", "viewOfferings", "viewMyData", "home", "viewMyOrders" ]
  }
}, {
  "id" : 6,
  "email" : "[email protected]",
  "password" : "ukmedpass",
  "org" : "University of Kentucky College of Medicine",
  "role" : {
    "id" : 2,
    "name" : "ADMIN",
    "views" : [ "viewPublicReports", "viewAllOrders", "viewProducts", "orderProducts", "viewOfferings", "viewMyData", "viewAllData", "home", "viewMyOrders", "manageUsers" ]
  }
} ]

Ключевым моментом является осознание того, что Spring не просто делает это автоматически для вас. Если вы просто попросите его вернуть элемент Account без выполнения работы вложенных объектов, вы просто получите:

{
  "id" : 6,
  "email" : "[email protected]",
  "password" : "ukmedpass",
  "org" : "University of Kentucky College of Medicine",
  "role" : null
}

Итак, во-первых, создайте свой запрос SQL JOIN с тремя таблицами и убедитесь, что вы получаете все необходимые данные. Здесь мой, как он появляется в моем контроллере:

@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@RequestMapping("/accounts")
public List<Account> getAllAccounts3() 
{
    List<Account> accounts = jdbcTemplate.query("SELECT Account.id, Account.password, Account.org, Account.email, Account.role_for_this_account, Role.id AS roleid, Role.name AS rolename, role_views.role_id, role_views.views FROM Account JOIN Role on Account.role_for_this_account=Role.id JOIN role_views on Role.id=role_views.role_id", new AccountExtractor() {});
    return accounts;
}

Обратите внимание, что я ПРИСОЕДИНИЛ 3 таблицы. Теперь создайте класс RowSetExtractor для объединения вложенных объектов. Вышеприведенные примеры показывают 2-слойную вложенность... этот шаг идет дальше и делает 3 уровня. Обратите внимание, что мне также нужно поддерживать объект второго уровня на карте.

public class AccountExtractor implements ResultSetExtractor<List<Account>>{

    @Override
    public List<Account> extractData(ResultSet rs) throws SQLException, DataAccessException {

        Map<Long, Account> accountmap = new HashMap<Long, Account>();
        Map<Long, Role> rolemap = new HashMap<Long, Role>();

        // loop through the JOINed resultset. If the account ID hasn't been seen before, create a new Account object. 
        // In either case, add the role to the account. Also maintain a map of Roles and add view (strings) to them when encountered.

        Set<String> views = null;
        while (rs.next()) 
        {
            Long id = rs.getLong("id");
            Account account = accountmap.get(id);
            if(account == null)
            {
                account = new Account();
                account.setId(id);
                account.setPassword(rs.getString("password"));
                account.setEmail(rs.getString("email"));
                account.setOrg(rs.getString("org"));
                accountmap.put(id, account);
            }

            Long roleid = rs.getLong("roleid");
            Role role = rolemap.get(roleid);
            if(role == null)
            {
                role = new Role();
                role.setId(rs.getLong("roleid"));
                role.setName(rs.getString("rolename"));
                views = new HashSet<String>();
                rolemap.put(roleid, role);
            }
            else
            {
                views = role.getViews();
                views.add(rs.getString("views"));
            }

            views.add(rs.getString("views"));
            role.setViews(views);
            account.setRole(role);
        }
        return new ArrayList<Account>(accountmap.values());
    }
}

И это дает желаемый результат. POJO ниже для справки. Обратите внимание на элементы @ElementCollection Set в классе Role. Это то, что автоматически генерирует таблицу role_views, как указано в SQL-запросе. Зная, что таблица существует, ее имя и имена его полей имеют решающее значение для правильного запроса SQL-запроса. Чувствовать себя неправильно, чтобы знать, что... похоже, что это должно быть более автоматическим - разве это не то, что Spring?... но я не мог найти лучшего способа. Насколько я могу судить, вам нужно сделать работу вручную в этом случае.

@Entity
public class Account implements Serializable {
        private static final long serialVersionUID = 1L;

        @Id 
        @GeneratedValue(strategy=GenerationType.AUTO)
        private long id;
        @Column(unique=true, nullable=false)
        private String email;
        @Column(nullable = false) 
        private String password;
        @Column(nullable = false) 
        private String org;
        private String phone;

        @ManyToOne(fetch = FetchType.EAGER, optional = false)
        @JoinColumn(name = "roleForThisAccount") // @JoinColumn means this side is the *owner* of the relationship. In general, the "many" side should be the owner, or so I read.
        private Role role;

        public Account() {}

        public Account(String email, String password, Role role, String org) 
        {
            this.email = email;
            this.password = password;
            this.org = org;
            this.role = role;
        }
        // getters and setters omitted

    }

   @Entity
   public class Role implements Serializable {

        private static final long serialVersionUID = 1L;

        @Id 
        @GeneratedValue(strategy=GenerationType.AUTO)   
        private long id; // required

        @Column(nullable = false) 
        @Pattern(regexp="(ADMIN|USER)")
        private String name; // required

        @Column
        @ElementCollection(targetClass=String.class)
        private Set<String> views;

        @OneToMany(mappedBy="role")
        private List<Account> accountsWithThisRole;

        public Role() {}

        // constructor with required fields
        public Role(String name)
        {
            this.name = name;
            views = new HashSet<String>();
            // both USER and ADMIN
            views.add("home");
            views.add("viewOfferings");
            views.add("viewPublicReports");
            views.add("viewProducts");
            views.add("orderProducts");
            views.add("viewMyOrders");
            views.add("viewMyData");
            // ADMIN ONLY
            if(name.equals("ADMIN"))
            {
                views.add("viewAllOrders");
                views.add("viewAllData");
                views.add("manageUsers");
            }
        }

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

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

        public Set<String> getViews() { return this.views; }
        public void setViews(Set<String> views) { this.views = views; };
    }

Ответ 4

Немного поздно для партии, однако я нашел это, когда я искал один и тот же вопрос, и я нашел другое решение, которое может быть выгодным для других в будущем.

К сожалению, нет собственного способа достижения вложенного сценария без создания клиента RowMapper. Однако я поделюсь более простым способом сделать упомянутый пользовательский RowMapper, чем некоторые другие решения здесь.

С учетом вашего сценария вы можете сделать следующее:

class User {
    String user_name;
    String display_name;
}

class Message {
    String title;
    String question;
    User user;
}

public class MessageRowMapper implements RowMapper<Message> {

    @Override
    public Message mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = (new BeanPropertyRowMapper<>(User.class)).mapRow(rs,rowNum);
        Message message = (new BeanPropertyRowMapper<>(Message.class)).mapRow(rs,rowNum);
        message.setUser(user);
        return message;
     }
}

Ключевым моментом, который нужно запомнить с помощью BeanPropertyRowMapper, является то, что вы должны следовать присвоению имен столбцам и свойствам ваших членов класса букве со следующими исключениями (см. Spring Документация):

  • Имена столбцов сглажены точно Названия столбцов
  • с символами подчеркивания будут преобразованы в случай "верблюда" (т.е. MY_COLUMN_WITH_UNDERSCORES == myColumnWithUnderscores)

Ответ 5

Я много работал над такими вещами и не вижу элегантного способа достижения этого без устройства OR.

Любое простое решение, основанное на отражении, будет в значительной степени полагаться на соотношение 1:1 (или, возможно, N: 1). Кроме того, возвращаемые столбцы не соответствуют их типу, поэтому вы не можете сказать, какие столбцы соответствуют этому классу.

Вы можете уйти с spring -data и QueryDSL. Я не копал в них, но я думаю, вам нужны метаданные для запроса, который позже используется для отображения столбцов из вашей базы данных в правильную структуру данных.

Вы также можете попробовать новую поддержку postgresql json, которая выглядит многообещающей.

НТН