2013-05-23 19 views
12

Given następujący przykład POJO na: (Załóżmy pobierające i ustawiające dla wszystkich właściwości)JDBCTemplate ustawić zagnieżdżony POJO z BeanPropertyRowMapper

class User { 
    String user_name; 
    String display_name; 
} 

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

Można łatwo kwerendy bazy danych (PostgreSQL w moim przypadku) i wypełnić listę klas wiadomości przy użyciu obiektu BeanPropertyRowMapper, w którym pole db pasuje do właściwości w POJO: (Zakładając, że tabele DB mają odpowiednie pola do właściwości POJO).

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

Zastanawiam - czy wygodny sposób do budowy pojedynczego zapytania i/lub tworzyć element odwzorowujący rzędu w taki sposób, aby także wypełnić właściwości wewnętrznej „użytkownik” POJO w komunikacie.

Oznacza to, że niektóre syntatical magia gdzie każdy wiersz wynik w zapytaniu:

SELECT * FROM message, user WHERE user_id = message_id 

Produce listę wiadomości z powiązanego Użytkownika zaludnionych


przypadków użycia:

Ostatecznie , klasy są przekazywane z powrotem jako obiekt serializowany z kontrolera Spring, klasy są zagnieżdżane, tak że wynikowy JSON/XML ma przyzwoitą strukturę.

Obecnie sytuację rozwiązuje się, wykonując dwa zapytania i ręcznie ustawiając właściwość użytkownika każdej wiadomości w pętli. Użyteczne, ale wyobrażam sobie, że powinien być bardziej elegancki sposób.


Aktualizacja: Rozwiązanie Używany -

Kudos do @Will Keeling inspiracji na odpowiedź z wykorzystaniem odwzorowującego zwyczaj rzędzie - Moje rozwiązanie dodaje dodanie mapy własności fasola w celu zautomatyzowania zadań terenowych .

Zastrzeżenie jest strukturyzacji kwerendę tak, że odpowiednie nazwy tabel są prefiksem (jednak nie ma standardowej konwencji to zrobić tak kwerenda jest zbudowany programowo):

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 

Zwyczaj rząd odwzorowujący następnie tworzy kilka bean map i ustawia ich właściwości na podstawie przedrostka kolumny: (używając meta danych, aby uzyskać nazwę kolumny).

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; 
} 

Ponownie, może się to wydawać przesadą w przypadku łączenia dwóch klas, ale przypadek użycia IRL obejmuje wiele tabel z dziesiątkami pól.

+1

Powyżej fragment kodu jest uszkodzony. Jest więcej zamków zamykających niż otwierających. (4 vs 3) – fivedogit

+0

@fivedogit - zaktualizowano - TY- mam nadzieję, że uznałeś to za przydatne – nclord

Odpowiedz

8

Wiosna wprowadził nową AutoGrowNestedPaths własności do interfejsu BeanMapper.

Dopóki zapytanie SQL formatuje nazwy kolumn za pomocą. separator (tak jak poprzednio), wówczas program odwzorowujący Rzędy automatycznie wybierze obiekty wewnętrzne.

Z tym, że stworzył nowy generic wierszy mapowania następująco:

zapytanie:

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; 
    } 
} 
+0

Nie działa, kolumna i wartość są poprawnie wypełnione, ale zwrócony obiekt mapowanyObject jest pełny wartości pustej. – Accollativo

2

Dużo pracowałem nad takimi rzeczami i nie widzę eleganckiego sposobu, aby to osiągnąć bez mapera OR.

Każde proste rozwiązanie oparte na refleksji byłoby w dużym stopniu zależne od relacji 1: 1 (lub może N: 1). Ponadto zwrócone kolumny nie są kwalifikowane według ich typu, więc nie można powiedzieć, które kolumny odpowiadają danej klasie.

Możesz uciec z danych sprężyn i QueryDSL. Nie zagłębiałem się w nie, ale myślę, że potrzebujesz trochę meta-danych dla zapytania, które jest później używane do odwzorowania kolumn z bazy danych do odpowiedniej struktury danych.

Możesz również wypróbować nowe wsparcie dla Postgresql json, które wygląda obiecująco.

HTH

+0

Dzięki za sugestie - dam im Google i odpiszę tutaj, jeśli okaże się przydatny. Jakieś zmiany w opracowywaniu mapowania OR? – nclord

+1

Nie zastosowałem tego jeszcze ... po prostu rozmawiałem z facetem, który przyczynił się do wiosny-jpa (np. Hades, jeśli googlecie). ma sens dla mnie. –

+0

TY na smyczy - mogę to zbadać w przyszłości – nclord

11

Być może przekazać w zwyczaju RowMapper które mogą mapować każdy wiersz agregatu dołączyć zapytanie (między wiadomości i użytkownika) do Message i zagnieżdżone User. Coś takiego:

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; 
    } 
}); 
+1

Dzięki za sugestię - miałem nadzieję, że skorzystam z refleksji; Zamieszczony przykład był dla zwięzłości - sytuacja IRL ma POJO z dziesiątkami różnych typów pól. – nclord

+1

Przegłosowane - może to być użyteczny przykład programu odwzorowującego wiersze dla prostego zagnieżdżonego POJO. – nclord

+0

Proszę zobaczyć aktualizacje w oryginalnym pytaniu – nclord

3

Aktualizacja: 04.10.2015. Zazwyczaj nie robię już żadnego z tych wierszy. Możesz przeprowadzić selektywną reprezentację JSON znacznie bardziej elegancko dzięki adnotacjom. Zobacz tę gist.


Spędziłem lepszą część całego dnia próbując rozgryźć to dla mojego przypadku 3-warstwowych obiektów zagnieżdżonych i właśnie w końcu go przybiłem. Oto moja sytuacja:

Konta (tj użytkowników) --1tomany -> Role --1tomany -> widoki (użytkownik może zobaczyć)

(. Te klasy POJO są wklejane na samym dole)

chciałem kontroler, aby powrócić do obiektu tak:

[ { 
    "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" ] 
    } 
} ] 

Kluczową kwestią jest uświadomienie sobie, że wiosna nie tylko robić to automatycznie wszystko dla Ciebie. Jeśli po prostu poprosić go, aby powrócić element konta bez wykonywania prac zagnieżdżonych obiektów, można jedynie dostać:

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

Więc, po pierwsze, stworzyć swój 3-tabeli SQL JOIN kwerendy i upewnij się, że uzyskanie wszystkie potrzebne dane. Oto mój, jak się pojawia w moim kontrolerze:

@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; 
} 

Pamiętaj, że ŁĄCZĘ 3 stoły. Teraz utwórz klasę RowSetExtractor, aby połączyć zagnieżdżone obiekty. Powyższe przykłady pokazują 2-warstwowe zagnieżdżanie ... ten idzie o krok dalej i robi 3 poziomy. Zwróć uwagę, że muszę również zachować obiekt drugiej warstwy na mapie.

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()); 
    } 
} 

a to daje pożądany wynik. POJO poniżej dla odniesienia. Zwróć uwagę na widoki zestawów @ElementCollection w klasie Role. To właśnie automatycznie generuje tabelę widoków ról, do której odwołuje się zapytanie SQL. Wiedząc, że tabela istnieje, jej nazwa i nazwy pól są kluczowe dla prawidłowego uzyskania zapytania SQL. Czuje się źle, wiedząc, że ... wydaje się, że powinno to być bardziej zautomatyzowane - czyż nie jest to tym, czym jest Wiosna? ... ale nie mogłem wymyślić lepszego sposobu. Musisz wykonać pracę ręcznie w tym przypadku, o ile wiem.

@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; }; 
    } 
3

Trochę późno na imprezę jednak Znalazłem to, gdy szukałem tego samego pytania i znalazłem inne rozwiązanie, które może być korzystne dla innych użytkowników przyszłość.

Niestety, nie ma natywnego sposobu na osiągnięcie zagnieżdżonego scenariusza bez utworzenia klienta RowMapper. Jednak udostępnię łatwiejszy sposób tworzenia niestandardowego programu RowMapper niż niektóre inne rozwiązania tutaj.

Biorąc pod uwagę Twój scenariusz można wykonać następujące czynności:

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; 
    } 
} 

Kluczem jest, aby pamiętać o BeanPropertyRowMapper jest to, że trzeba podążać za nazwanie swoich kolumnach i właściwości swoich członków klasy do pisma z następujące wyjątki (see Spring Documentation):

  • nazwy kolumn są aliasem dokładnie
  • nazwy kolumn podkreślenia zostaną zamienione na „wielbłąda” sprawy (tj MY_CO. LUMN_WITH_UNDERSCORES == myColumnWithUnderscores)
+0

Przyjemne ulepszenie! Chciałbym dodać następującą propozycję: - Przechowuj 'BeanPropertyRowMapper's' w prywatnych końcowych polach dla lepszej wydajności ponownego użycia głównej metody' mapRow'. –