multiple one-to-many relations ResultSetExtractor - java

Let's say I have an object with two different one-to-many relations. Much like:
Customer 1<->M Brands and Customer 1<->M Orders
And let's say that the my object Customer has two lists related to those two objects.
I've read this example:
http://forum.springsource.org/showthread.php?50617-rowmapper-with-one-to-many-query
which explains how to do it with a single one-to-many relationship. For your convenience here's the ResultSetExtractor override:
private class MyObjectExtractor implements ResultSetExtractor{
public Object extractData(ResultSet rs) throws SQLException, DataAccessException {
Map<Integer, MyObject> map = new HashMap<Integer, MyObject>();
MyObject myObject = null;
while (rs.next()) {
Integer id = rs.getInt("ID);
myObject = map.get(id);
if(myObject == null){
String description = rs,getString("Description");
myObject = new MyObject(id, description);
map.put(id, myObject);
}
MyFoo foo = new MyFoo(rs.getString("Foo"), rs.getString("Bar"));
myObject.add(myFoo);
}
return new ArrayList<MyObject>(map.values());;
}
}
I don't think it covers how to work with both. What would be the cleanest approach? Is there a simpler way than to iterate with conditions? Would sets be better off than lists in this case?

From your question, I assume that you have three tables; Customer, Brands, Orders. If you want to fetch the Brands and Orders properties of the Customer to your customer object, where there is no relationship between Brands and Orders, what I suggest is to use a UNION query. Something like this:
TBL_CUSTOMER
------------
CUSTOMER_ID
CUSTOMER_ACCOUNT_NO
CUSTOMER_NAME
TBL_CUSTOMER_BRANDS
-------------------
CUSTOMER_BRAND_ID - UK
BRAND_NAME
CUSTOMER_ID - FK
TBL_ORDERS
-------------------
ORDER_ID - UK
CUSTOMER_ID - FK
Query:
SELECT CUS.*, BRANDS.CUSTOMER_BRAND_ID COL_A, BRANDS.BRAND_NAME COL_B, 1 IS_BRAND FROM TBL_CUSTOMER CUS JOIN TBL_CUSTOMER_BRANDS BRANDS ON (CUS.CUSTOMER_ID = BRANDS.CUSTOMER_ID)
UNION ALL
SELECT CUS.*, ORDERS.ORDER_ID, '', 0 IS_BRAND FROM TBL_CUSTOMER CUS JOIN TBL_ORDERS ORDERS ON (CUS.CUSTOMER_ID = ORDERS.CUSTOMER_ID)
Your ResultSetExtractor will become:
private class MyObjectExtractor implements ResultSetExtractor{
public Object extractData(ResultSet rs) throws SQLException, DataAccessException {
Map<Long, Customer> map = new HashMap<Long, Customer>();
while (rs.next()) {
Long id = rs.getLong("CUSTOMER_ID");
Customer customer = map.get(id);
if(customer == null){
customer = new Customer();
customer.setId(id);
customer.setName(rs.getString("CUSTOMER_NAME"));
customer.setAccountNumber(rs.getLong("CUSTOMER_ACCOUNT_NO"));
map.put(id, customer);
}
int type = rs.getInt("IS_BRAND");
if(type == 1) {
List brandList = customer.getBrands();
if(brandsList == null) {
brandsList = new ArrayList<Brand>();
customer.setBrands(brandsList);
}
Brand brand = new Brand();
brand.setId(rs.getLong("COL_A"));
brand.setName(rs.getString("COL_B"));
brandsList.add(brand);
} else if(type == 0) {
List ordersList = customer.getOrders();
if(ordersList == null) {
ordersList = new ArrayList<Order>();
customer.setOrders(ordersList);
}
Order order = new Order();
order.setId(rs.getLong("COL_A"));
ordersList.add(order);
}
}
return new ArrayList<Customer>(map.values());
}
}

I think there is no better way than to iterate over all rows, extract the two different objects and add it to a List<Brand> and List<Order> within the Customer object.
So you would end up in a customer object:
public class Customer {
private List<Brand> brands;
private List<Order> orders;
....
}
There was an issue on SpringSource regarding a mutliple rowmapper: https://jira.springsource.org/browse/SPR-7698
but there's only one comment linking to a one-to-many resultset extractor:
https://github.com/SpringSource/spring-data-jdbc-ext/blob/master/spring-data-jdbc-core/src/main/java/org/springframework/data/jdbc/core/OneToManyResultSetExtractor.java
I think you're doing it right if you really need eager fetching.
If you'd need lazy fetching you could load the respective orders and brands on access during runtime. That's how Hibernate and other ORM frameworks do it. It depends on your scenario and what you do with the object.

I assume the model described by James Jithin in his answer:
TBL_CUSTOMER
------------
CUSTOMER_ID
CUSTOMER_ACCOUNT_NO
CUSTOMER_NAME
TBL_CUSTOMER_BRANDS
-------------------
CUSTOMER_BRAND_ID - UK
BRAND_NAME
CUSTOMER_ID - FK
TBL_ORDERS
-------------------
ORDER_ID - UK
CUSTOMER_ID - FK
Instead of going for one Query, I would suggest the following three:
SELECT CUS.* FROM TBL_CUSTOMER CUS
SELECT BRANDS.CUSTOMER_ID, BRANDS.CUSTOMER_BRAND_ID, BRANDS.BRAND_NAME FROM TBL_CUSTOMER_BRANDS BRANDS
SELECT ORDERS.CUSTOMER_ID, ORDERS.ORDER_ID FROM TBL_ORDERS ORDERS
Your RowCallbackHandlers would become:
private class CustomerRowCallbackHandler implements RowCallbackHandler {
private final Map<Long, Customer> customerMap;
public BrandRowCallbackHandler(Map<Long, Customer> customerMap) { this.customerMap = customerMap}
public void processRow(ResultSet rs) throws SQLException {
Long id = rs.getLong("CUSTOMER_ID");
Customer customer = map.get(id);
if(customer == null){
customer = new Customer();
customer.setId(id);
customer.setName(rs.getString("CUSTOMER_NAME"));
customer.setAccountNumber(rs.getLong("CUSTOMER_ACCOUNT_NO"));
map.put(id, customer);
}
}
}
private class BrandRowCallbackHandler implements RowCallbackHandler {
private final Map<Long, Customer> customerMap;
public BrandRowCallbackHandler(Map<Long, Customer> customerMap) { this.customerMap = customerMap}
public void processRow(ResultSet rs) throws SQLException {
Long id = rs.getLong("CUSTOMER_ID");
Customer customer = map.get(id);
if(customer != null){
List brandList = customer.getBrands();
if(brandsList == null) {
brandsList = new ArrayList<Brand>();
customer.setBrands(brandsList);
}
Brand brand = new Brand();
brand.setId(rs.getLong("CUSTOMER_BRAND_ID"));
brand.setName(rs.getString("CUSTOMER_BRAND_NAME"));
brandsList.add(brand);
}
}
}
private class OrderRowCallbackHandler implements RowCallbackHandler {
private final Map<Long, Customer> customerMap;
public OrderRowCallbackHandler(Map<Long, Customer> customerMap) { this.customerMap = customerMap}
public void processRow(ResultSet rs) throws SQLException {
Long id = rs.getLong("CUSTOMER_ID");
Customer customer = map.get(id);
if(customer != null){
List ordersList = customer.getOrders();
if(ordersList == null) {
ordersList = new ArrayList<Order>();
customer.setOrders(ordersList);
}
Order order = new Order();
order.setId(rs.getLong("ORDER_ID"));
ordersList.add(order);
}
}
}

If I really had to do it, I would prefer RowCallbackHandler over ResultSetExtractor. See RowCallbackHandler api and JDBCTemplate api.
In this case you need to collect the resulting Customers collection yourself in the handler.
Sets can help to filter out duplicates.

Related

Batch fetching into #ElementCollection

I have following entity with its persistent collection
#Entity
#Table(name = "A")
public class A implements Identifiable<Long> {
#Id
private Long id;
#ElementCollection
#CollectionTable(name = "B", joinColumns = { #JoinColumn(name = "B_ID") })
private Collection<B> bList;
#ElementCollection
#CollectionTable(name = "C", joinColumns = { #JoinColumn(name = "C_ID") })
private Collection<C> cList;
}
After loading 10k rows A entities, I want to load its collection as well
// loading A entities
final List<A> aList = getA();
// looping from 10k results
for (final A a : aList) {
final List<B> bList = a.getB();
final List<C> cList = a.getC();
}
And select statement generated quite a lot (~10k).
Very poor performance here!
Any idea to work with batch select here?
I have solved this!
IDEA
Hibernate will take care of the sql statement and mapping to entity list value when using #ElementCollection. That’s comfortable to use but we have a trade off.
The more parent results we have, the worse performance we got. If we have 10k records parent, Hibernate will do selecting 10k times to fetch its children relation.
Instead of loading children for every single parent. Create native query to load everything.
we got the results like this:
PARENT_ID CHILD_ID
1 1
1 2
1 3
2 1
2 2
3 3
then implementing Hibernate transformer to convert these raw database objects to DTO.
Code example.
Create DTO
public class ADto {
private long id;
private Collection<BDto> bList = new HashSet<>();
// Constructor
public void addChildren(BDto b) {
bList.add(b);
}
//equals and hascode
}
public class BDto {
private long id;
// Constructor
//equals and hascode
}
And transformer
public class CustomTransformer extends AliasedTupleSubsetResultTransformer {
private final Map<Long, ADto> result = new HashMap<>();
private final Map<String, Integer> aliasIndexes = new HashMap<>();
#Override
public List transformList(final List list) {
return new ArrayList(new LinkedHashSet(list));
}
#Override
public UsableCapacity transformTuple(final Object[] tuple, final String[] aliases) {
init(aliases);
final A aEntity = (A) get(tuple, "parent"); // same as alias in DAO layer
final B bEntity = (B) get(tuple, "child"); // same as alias in DAO layer
final Long id = aEntity.getId();
final ADto aDto;
if (result.containsKey(id)) {
aDto = result.get(id);
} else {
aDto = new ADto(...);
}
aDto.addChildren(new BDto(...)); // create BDto instance from BEntity
result.put(id, aDto);
return aDto;
}
private Object get(final Object[] capacities, final String alias) {
return capacities[aliasIndexes.get(alias)];
}
private void init(final String[] aliases) {
if (aliasIndexes.isEmpty()) {
for (int i = 0; i < aliases.length; i++) {
final String alias = aliases[i];
aliasIndexes.put(alias, i);
}
}
}
}
DAO layer
final String queryString = "SELECT {parent.*}, {child.*} FROM A parent LEFT JOIN B child ON parent.id = child.parent_id";
final NativeQuery query = getCurrentSession().createNativeQuery(queryString)
.addEntity("parent", A.class)
.addEntity("child", B.class);
// Todo
query.setResultTransformer(new CustomTransformer());
return safeList(query);

Passing in a Class dynamically

Hopefully quite a simple question. How do I pass in a Class into an abstract method?
My abstract method is as follows:
public abstract class IDataList {
public LinkedList<IThing> getRows(IThing thing, String sql, List<Object> vals) throws Exception {
LinkedList<IThing> list = new LinkedList<>();
List<LinkedHashMap<String, Object>> rows = db.executeSelect(sql, vals);
for (HashMap<String, Object> row : rows) {
list.add(new thing(row));
}
rowCount = (long) getDb().executeScalar("SELECT FOUND_ROWS()");
return list;
}
}
Which is inherited by a concrete class:
public class DataList extends IDataList {
}
The IThing is currently an empty abstract class which is extended by an Thing, for example:
public class Thing extends IThing {
private long uid;
private String name;
public Thing(HashMap<String, Object> row) {
this.uid = (long) row.get("uid");
this.name = (String)row.get("name");
}
}
I want to be able to pass in Thing into a concrete class of IDataList, for example:
IDataList dataList = new DataList();
dataList.getRows(Thing, "select something", new ArrayList<>())
If you want to use Thing Class as a Row->Object mapper, it will be better to use org.springframework.jdbc.core.RowMapper
public class CustomerRowMapper implements RowMapper {
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
Customer customer = new Customer();
customer.setCustId(rs.getInt("CUST_ID"));
customer.setName(rs.getString("NAME"));
customer.setAge(rs.getInt("AGE"));
return customer;
}
}
public Customer findByCustomerId(int custId) {
String sql = "SELECT * FROM CUSTOMER WHERE CUST_ID = ?";
Customer customer = (Customer)getJdbcTemplate().queryForObject(sql, new Object[] { custId }, new CustomerRowMapper());
return customer;
}

Spring jdbcTemplate data access and java algorithms

I have one method which must return data in DAO.
Department model:
public class Department implements Serializable {
private Long id;
private String departmentName;
private List<Employees> employeesInThisDepartment;
// getters and setters...
}
Employees model:
public class Employees implements Serializable {
private Long id;
private String department;
private String fullName;
private Date birthday;
private int salary;
// getters and setters...
}
And method in DAO:
#Override
public Department findByDepartmentNameWithEmployees(String departmentName) {
String sql = "select d.id, d.departmentName, e.id, e.fullName, e.department, e.birthday" +
", e.salary from department as d left join employees as e on d.departmentName = e.department " +
"where lower(d.departmentName) = lower(:departmentName)";
Map<String, Object> map = new HashMap<>();
map.put("departmentName", departmentName);
return jdbcTemplate.queryForObject(sql, map, (rs, rowNum) -> {
Department department = new Department();
department.setId(rs.getLong("department.id"));
department.setDepartmentName(rs.getString("department.departmentName"));
department.setEmployeesInThisDepartment(new ArrayList<>());
while (rs.next()){
Employees employees = new Employees();
employees.setId(rs.getLong("employees.id"));
employees.setFullName(rs.getString("employees.fullName"));
employees.setDepartment(rs.getString("employees.department"));
employees.setBirthday(rs.getDate("employees.birthday"));
employees.setSalary(rs.getInt("employees.salary"));
department.getEmployeesInThisDepartment().add(employees);
}
return department;
});
}
This method must return one department with list of all employees who works in this department, but it misses the first employee in the list.
Why is this happening? (SQL part is working correctly, I think that the problem is with the loop?)
Right, problem in the loop. According to docs:
Implementations must implement this method to map each row of data in
the ResultSet.
So you do not need to invoke rs.next(), just remove this loop wrapper and move block
Department department = new Department();
department.setEmployeesInThisDepartment(new ArrayList<>());
outside return jdbcTemplate.queryForObject(sql, map, (rs, rowNum) -> { ... }. I.e.:
Department department = new Department();
department.setEmployeesInThisDepartment(new ArrayList<>());
jdbcTemplate.queryForObject(sql, map, (rs, rowNum) -> {
department.setId(rs.getLong("department.id"));
department.setDepartmentName(rs.getString("department.departmentName"));
Employees employees = new Employees();
employees.setId(rs.getLong("employees.id"));
employees.setFullName(rs.getString("employees.fullName"));
employees.setDepartment(rs.getString("employees.department"));
employees.setBirthday(rs.getDate("employees.birthday"));
employees.setSalary(rs.getInt("employees.salary"));
department.getEmployeesInThisDepartment().add(employees);
});
return department;

how to use map to reduce objects creation in spring MVC web app?

Situation : I am designing a spring MVC based web app , i have a table called customers it consists of 3 columns id , property , property value .
id is not primary key.
Following is the Model class i am using :
public class prop {
private String id;
private String property;
private String property_value;
/*setter and getters of these three variables ...*/
}
and my Dao is :
#Repository("Dao")
public class Dao implements{
#Autowired
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<Model> listProp(String id) {
final String sql = "select * from customers where id = ? ";
final List<Model> list = jdbcTemplate.query(sql, new Object[]{id}, new Mapper());
return list;
}
}
And my Mapper class is :
public class Mapper implements RowMapper<Model> {
public Model mapRow(ResultSet rs, int rowNum) throws SQLException {
Model m = new Model();
m.setId(rs.getString(1));
m.setProperty(rs.getString(2));
m.setValue(rs.getString(3));
return wl;
}
}
Problem :Now i have a scenario in which id=1 has 4 properties , so it will have 4 corresponding rows and 4 Model objects are created,
if id=1 has say 100 properties , then 100 model objects are created which is inefficient , i want that , for all rows with id=1 one Modelobject must be created , i tried using map but couldn't implement it properly can somebody please help ?
Note: In UI i am displaying all the records as they are present in DB
You can modify class prop as
public class prop {
private String property;
private String property_value;
/*setter and getters of these three variables ...*/
}
Modify Mapper code accordingly to use HashMap.
public class Mapper {
Map<String ,List<Model>> map = new HashMap<String , List<Model>>();
public Map<String , Model> mapRow(ResultSet rs, int rowNum) throws SQLException {
Model m = new Model();
m.setProperty(rs.getString(2));
m.setValue(rs.getString(3));
if(map.containsKey(rs.getString(1))) {
List<Model> modelList = map.get(rs.getString(1));
modelList.add(m);
} else {
List<Model> modelList = new ArrayList<Model>();
modelList.add(m);
map.put(rs.getString(1),modelList);
}
return map;
}
}

ParameterizedRowMapper That Maps Object List to Object

I am trying to set the Parent List in a ParameterizedRowMapper how is this written or approached. I have two Objects one for parent and one for children however children contains a ListThe parents for each child are stored in a separate table in the database and the mapping is 1 - many.
The select for the records for the parents will be done in a separate ResultSet. Will the mapping have to be done separately (separate ParameterizedRowMapper), if so how will i have to write the ParameterizedRowMapper this is the major concern how ParameterizedRowMapper is written to accommodate a list items.
ParameterizedRowMapper
public static class ChildrenMapper implements ParameterizedRowMapper<Children>{
public Children mapRow(ResultSet rs, int rowNum) throws SQLException {
Children child = new Children();
child.setFirstName(rs.getString("firstName"));
child.setLastName(rs.getString("lastName"));
//a child can have many Parents or gaurdians
child.setParent(List<Parent>);
return child;
}
}
Based on my research i have found that i need to use ResultSetExtractor, however i have a questions on the use of that. Do i integrate it into the class at the point of setting the Parent? Can someone guide me on how it can be done the correct way
Children.java
Public class Children(){
int cid;
String firstName;
String lastName;
List<Parent>parents;
..
//getters/setters
}
Parent.java
Public class Parent(){
int pid;
String firstName;
String lastName;
..
//setters/getters
}
I will show how to do this for a canonical 1-to-many example, you can adapt it to your vo class / table.
Order class
public class Order {
private Long orderId;
private String user;
private List<LineItem> items;
// Getter / setter omitted
}
Item class
public class LineItem {
private Long lineItemId;
private String product;
private int quantity;
// Getter / setter omitted
}
Use two rowmappers one for each class and then use a result set extractor to convert multiple rows into one order + line items
OrderRepository
public final static RowMapper<Order> orderMapper = ParameterizedBeanPropertyRowMapper.newInstance(Order.class);
public final static RowMapper<LineItem> lineItemMapper = ParameterizedBeanPropertyRowMapper.newInstance(LineItem.class);
public Order findOrderWithItems(Long orderId) {
return jdbcTemplate.query("select * from orders, line_item "
+ " where orders.order_id = line_item.order_id and orders.order_id = ?",
new ResultSetExtractor<Order>() {
public Order extractData(ResultSet rs) throws SQLException, DataAccessException {
Order order = null;
int row = 0;
while (rs.next()) {
if (order == null) {
order = orderMapper.mapRow(rs, row);
}
order.addItem(lineItemMapper.mapRow(rs, row));
row++;
}
return order;
}
}, orderId);
}
public List<Order> findAllOrderWithItmes() {
return jdbcTemplate.query("select * from orders, line_item "
+ " where orders.order_id = line_item.order_id order by orders.order_id",
new ResultSetExtractor<List<Order>>() {
public List<Order> extractData(ResultSet rs) throws SQLException, DataAccessException {
List<Order> orders = new ArrayList<Order>();
Long orderId = null;
Order currentOrder = null;
int orderIdx = 0;
int itemIdx = 0;
while (rs.next()) {
// first row or when order changes
if (currentOrder == null || !orderId.equals(rs.getLong("order_id"))) {
orderId = rs.getLong("order_id");
currentOrder = orderMapper.mapRow(rs, orderIdx++);
itemIdx = 0;
orders.add(currentOrder);
}
currentOrder.addItem(lineItemMapper.mapRow(rs, itemIdx++));
}
return orders;
}
});
}

Categories