How to update entity with OneToMany relationship in Spring Boot - java

I'm new to Spring boot and I'm writing an application to practice, with Postresql as db.
The application is about aviation. There are 4 entity: Airline, Airplane, Airport and Flight.
I tried GET, POST and DELETE requests and all works on Airline, Airplane and Airport, but I have a problem trying to add and update Flight.
Flight has 4 ManyToOne relationships, this is the class:
#Entity
#Table
public class Flight {
#Id
#Column(name = "flight_number")
private String flightNumber;
#ManyToOne
#JoinColumn(name = "origin")
private Airport origin;
#ManyToOne
#JoinColumn(name = "destination")
private Airport destination;
#Column(name = "departure_time")
private Timestamp departureTime;
#Column(name = "arrival_time")
private Timestamp arrivalTime;
#ManyToOne
#JoinColumn(name = "airline")
private Airline airline;
#ManyToOne
#JoinColumn(name = "airplane")
private Airplane airplane;
private Time duration;
private int passengers;
...
}
What I don't understand is how to insert and update this entity without the need to pass the Objects (Airline and Airport) but only the foreign keys (as when working directly on db).
Here is my code for add a new flight that requires the objects:
public void addFlight(Flight flight) {
boolean exists = flightRepository.existsById(flight.getFlightNumber());
if (exists) {
throw new IllegalStateException("Flight with number" + flight.getFlightNumber()
+ "already exists");
}
flightRepository.save(flight);
}
I also tried writing a specific query for the update in the repository and then call on PUT request, but I get some errors saying that I need to pass Airline and Airport, not strings that represents the IDs of these 2 entities.
*#Modifying
#Query("update Flight " +
"set origin = :origin, " +
"destination = :destination, " +
"departureTime = :departureTime, " +
"arrivalTime = :arrivalTime, " +
"airline = :airline, " +
"airplane = :airplane, " +
"passengers = :passengers, " +
"duration = :duration " +
"where flightNumber = :flightNumber")
void updateFlight(#Param("flightNumber") String flightNumber,
#Param("origin") String origin,
#Param("destination") String destination,
#Param("departureTime") Timestamp departureTime,
#Param("arrivalTime") Timestamp arrivalTime,
#Param("airline") String airline,
#Param("airplane") Long airplane,
#Param("passengers") Integer passengers,
#Param("duration") Time duration);*/
To summarize: I want to know if there is a method to avoid passing the entire Object that represents the ManyToOne relationship during creation and update.

Is the method addFlight(Flight flight) called from your controller? If that's the case, you should get a DTO from your controller that you'll map in an entity in your service layer.
From the DTO you'll need to provide the ids of Airline, Airplane and Airport objects so that you can retrieve them from the DB and set them on the Flight entity.
Here is a solution I can suggest:
void addFlight (FlightRequest flightRequest) {
boolean exists = flightRepository.existsById(flightRequest.flightNumber());
if (exists) {
throw new IllegalStateException("Flight with number" + flight.getFlightNumber()
+ "already exists");
}
Flight flight = new Flight();
airportRepository.findById(flightRequest.airportOriginId())
.ifPresentOrElse(airportOrigin -> {
flight.setAirportOrigin(airportOrigin);
},
() -> new IllegalStateException(...)
);
airportRepository.findById(flightRequest.airportDestinationId())
.ifPresentOrElse(airportDestination -> {
flight.setAirportDestination(airportDestination);
},
() -> new IllegalStateException(...)
);
airlineRepository.findById(flightRequest.airlineId())
.ifPresentOrElse(airline -> {
flight.setAirline(airline);
},
() -> new IllegalStateException(...)
);
airplaneRepository.findById(flightRequest.airplaneId())
.ifPresentOrElse(airplane -> {
flight.setAirplane(airplane);
},
() -> new IllegalStateException(...)
);
flight.setFlightNumber(flightRequest.flightNumber());
flight.setDepartureTime(flightRequest.departureTime());
flight.setArrivalTime(flightRequest.arrivalTime());
flight.setDuration(flightRequest.duration());
flight.setPassengers(flightRequest.passengers());
...
flightRepository.save(flight);
}
public record FlightRequest(
String flightNumber,
String airportOriginId,
String airportDestinationId,
Timestamp departureTime,
Timestamp arrivalTime,
String airlineId,
String airplaneId,
Time duration,
int passengers,
...
) {
}
And for updating your Flight, you could do it like this:
Flight updateFlight (FlightRequest flightRequest) {
Flight flight = flightRepository.findById(flightRequest.flightNumber())
.orElseThrow(...);
// Same as above without the call for existsById
...
}
If you want to go further, since version 2.7 of Spring Data JPA, you can use getReferenceById method instead of findById. In that case Spring Data JPA won't generate an SQL statement to get the entity, in our case we only want to set the foreign keys. You can find more about this here

Related

How do I add a foreign key to my entity in Room 2.3.0?

I have a project that I have worked with Room 2.2.5, I just updated to version 2.3.0 this is the code of an entity called photo:
#Entity(tableName = Photo.TABLE_NAME, foreignKeys = #ForeignKey(entity = Event.class,
parentColumns = "_id",
childColumns = "id_event_ft",
onDelete = ForeignKey.CASCADE))
public class Photo {
public static final String TABLE_NAME = "Photo";
public static final String COLUMN_ID = BaseColumns._ID;
#PrimaryKey(autoGenerate = true)
#ColumnInfo(index = true, name = COLUMN_ID)
public Long id_photo;
#ColumnInfo(name = "path")
private String path;
#ForeignKey(entity = Event.class,
parentColumns = "_id",
childColumns = "id_event_ft",
onDelete = ForeignKey.CASCADE)
private Long id_event_ft;
public Photo(Long id_photo, String path, Long id_event_ft) {
this.id_photo = id_photo;
this.path = path;
this.id_event_ft = id_event_ft;
}
public Long getId_photo() {
return id_photo;
}
public void setId_photo(Long id_photo) {
this.id_photo = id_photo;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public Long getId_event_ft() {
return id_event_ft;
}
public void setId_event_ft(Long id_event_ft) {
this.id_event_ft = id_event_ft;
}
}
Now I am getting the following error when trying to compile
error: annotation type not applicable to this kind of declaration
#ForeignKey(entity = Event.class, parentColumns = "_id", childColumns = "id_event_ft", onDelete = ForeignKey.CASCADE)
^
The error is in the #ForeignKey that is above the variable private Long id_event_ft;
In the documentation I found this:
Added missing target to #ForeignKey annotation preventing its usage outside of the #Entity annotation. (Iced1e)
It is clear that using #ForeignKey outside of the #Entity annotation is no longer allowed, but then how do I bind the id_event_ft variable to the foreign key?, How do I assign a value to it now?
I hope someone can help me, thank you very much
Using ForeignKey does not automatically (magically) make a relationship. Rather it allows a relationship to be supported primarily by enforcing referential integrity.
There is no need for a ForeignKey definition for a Foreign Key (relationship) to exist.
That is it is defining a rule that says that the value of the child column (id_event_ft) MUST be a value that is present in the parent column (_id). It also supports handling if there is a Foreign Key Conflict (e.g. onDelete as you have used).
Actually providing a suitable value is something that you have to do programmatically, that is id adding a photo you have to determine which Event the photo is to be linked/related to.
You can use #Relation to simplify extracting related data.
So consider the following:
An Event Entity (nice and simple for demonstration)
#Entity
public class Event {
#PrimaryKey(autoGenerate = true)
Long _id = null;
String other_columns;
public Event(){}
#Ignore
public Event(String other_columns) {
this.other_columns = other_columns;
}
}
A Photo's parent column will be the _id column.
Second Ignored (i.e. Ignored by Room) constructor otherwise Room issue a warning like *warning: There are multiple good constructors and Room will pick the no-arg constructor. *
A slightly changed Photo Entity :-
#Entity(tableName = Photo.TABLE_NAME,
foreignKeys = #ForeignKey(
entity = Event.class,
parentColumns = "_id",
childColumns = "id_event_ft",
onDelete = ForeignKey.CASCADE),
indices = #Index("id_event_ft") //<<<<<<<<<< ADDED as Room warns if omitted
)
public class Photo {
public static final String TABLE_NAME = "Photo";
public static final String COLUMN_ID = BaseColumns._ID;
#PrimaryKey(autoGenerate = true)
#ColumnInfo(index = true, name = COLUMN_ID)
public Long id_photo;
#ColumnInfo(name = "path")
private String path;
/* <<<<<<<< COMMENTED OUT >>>>>>>>>>
#ForeignKey(entity = Event.class,
parentColumns = "_id",
childColumns = "id_event_ft",
onDelete = ForeignKey.CASCADE)
*/
private Long id_event_ft;
public Photo(Long id_photo, String path, Long id_event_ft) {
this.id_photo = id_photo;
this.path = path;
this.id_event_ft = id_event_ft;
}
public Long getId_photo() {
return id_photo;
}
public void setId_photo(Long id_photo) {
this.id_photo = id_photo;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public Long getId_event_ft() {
return id_event_ft;
}
public void setId_event_ft(Long id_event_ft) {
this.id_event_ft = id_event_ft;
}
}
For demonstration of retrieving via a relationship the POJO EventWithPhotos :-
public class EventWithPhotos {
#Embedded
Event event;
#Relation(entity = Photo.class,parentColumn = "_id",entityColumn = "id_event_ft")
List<Photo> photos;
}
Now a Dao AllDao:-
#Dao
interface AllDao {
#Insert
long insert(Event event);
#Insert
long insert(Photo photo);
#Transaction
#Query("SELECT * FROM event")
List<EventWithPhotos> getAllEventsWithPhotos();
}
How do I assign a value to it now?
Now an example that puts it all together adding 2 Events the first with 2 photos, the second with 1 photo. Note the different techniques used:-
dao = db.getAllDao();
// Prepare to add an Event
Event newEvent = new Event();
newEvent.other_columns = "Event1";
// Add the Event retrieving the id (_id column)
long eventId = dao.insert(newEvent);
// Prepare a photo to be added to Event1
Photo newPhoto = new Photo(null,"photo1",eventId);
// Add the Photo to Event1
long photoid = dao.insert(newPhoto);
// Add second photo to Event 1 using the 2nd constructor
dao.insert(new Photo(null,"photo2",eventId));
// Add Event2 with a photo all in a single line (again using the 2nd constrcutor)
long event2Id;
dao.insert(new Photo(null,"photo3",event2Id = dao.insert(new Event("Event2"))));
// Get and output Each Event with the Photos for that Event
List<EventWithPhotos> allEventsWithPhotosList = dao.getAllEventsWithPhotos();
for (EventWithPhotos ewp: allEventsWithPhotosList) {
Log.d("EVENTPHOTOINFO","Event is " + ewp.event.other_columns);
for (Photo p: ewp.photos) {
Log.d("EVENTWITHPHOTO","\tPhoto is " + p.getPath() + " ID is " + p.getId_photo());
}
}
Result
When run the log contains :-
D/EVENTPHOTOINFO: Event is Event1
D/EVENTWITHPHOTO: Photo is photo1 ID is 1
D/EVENTWITHPHOTO: Photo is photo2 ID is 2
D/EVENTPHOTOINFO: Event is Event2
D/EVENTWITHPHOTO: Photo is photo3 ID is 3
The Database (view with Database Inspector) shows:-
The Event table :-
The Photo table :-
Relations
#Many To One
if the relation is many photo for one event, use ORM Hibernate, this is the easiest way to define foreign key with constraint on your Photo Entity
#ManyToOne
#JoinColumn(name = "id", nullable = false)
private Event id_event_ft;

Trying to avoid the JPA EntityListeners triggering for a OneToMany relationship

Hellos people, I have my entity
#Entity
#Getter
#Setter
#EntityListeners(TaskInfoEntityListener.class)
public class TaskInfo extends Auditable {
#Id
private String taskId;
...
#OneToMany(mappedBy = "task", orphanRemoval = true, fetch = FetchType.EAGER)
private List<TaskComment> comments;
}
And when I run this piece of code
final TaskInfo taskInfo = new TaskInfo();
taskInfo.setTaskId("1234");
final TaskComment taskComment = new TaskComment();
taskComment.setComment("comment_1234");
taskInfo.setComments(Collections.singletonList(taskComment));
TaskInfo newTaskInfo = taskInfoRepository.save(taskInfo);
The EntityListener is run twice (the first one when the TaskInfo is saved and the second one when the comments are saved), but I want it once (only the first time).
I´ve tried everything with the fetch and cascade in both sides but I can´t make it work. I also thought to put some kind of logic in the TaskInfoEntityListener in order to check whether or not the "comments" field changed but i don´t really like this solution.
What do you thing? Do you have a better approach or any other idea?
Thanks!
In your EntityListener, try providing lifecycle listeners for each step, and see what gets called. most likely you are getting called for different events. (maybe persist and update?)
#PrePersist
public void methodInvokedBeforePersist(TaskInfo ti) {
System.out.println("persisting TaskInfo with id = " + ti.getTaskId());
}
#PostPersist
public void methodInvokedAfterPersist(TaskInfo ti) {
System.out.println("Persisted TaskInfo with id = " + ti.getTaskId());
}
#PreUpdate
public void methodInvokedBeforeUpdate(TaskInfo ti) {
System.out.println("Updating TaskInfo with id = " + ti.getTaskId());
}
#PostUpdate
public void methodInvokedAfterUpdate(TaskInfo ti) {
System.out.println("Updated TaskInfo with id = " + ti.getTaskId());
}
#PreRemove
private void methodInvokedBeforeRemove(TaskInfo ti) {
System.out.println("Removing TaskInfo with id = " + ti.getTaskId());
}
#PostRemove
public void methodInvokedAfterRemove(TaskInfo ti) {
System.out.println("Removed TaskInfo with id = " + ti.getTaskId() );
}

Hibernate Ogm 2nd query based on 1st query result and also can't get mapped object fields(other than id) after update

i am using Hibernate OGM (5.2.0.Alpha1) with Mongodb (3.6)
#Entity
#Table(name = "currency_master)
#JsonInclude(Include.NON_EMPTY)
public class CurrencyMaster{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#JsonSerialize(using = ToStringSerializer.class)
#Column(name = "CURRENCY_ID", unique = true, nullable = false)
private ObjectId id;
private String code;
#OneToMany(mappedBy = "currencyMaster")
private Set<PurchaseOrder> setOfPurchaseOrder;
getter()
setter()
}
#Entity
#Table(name = "purchase_order)
#JsonInclude(Include.NON_EMPTY)
public class PurchaseOrder {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#JsonSerialize(using = ToStringSerializer.class)
#Column(name = "PURCHASE_ORDER_ID", unique = true, nullable = false)
private ObjectId id;
private String purchaseOrderNo;
private Double total;
#ManyToOne
#JsonIgnore
private CurrencyMaster currencyMaster;
getter()
setter()
}
DAO Layer:
#SuppressWarnings("unchecked")
#Override
public <T> void update(T t) {
try {
Field[] fields = t.getClass().getDeclaredFields();
Map<String, Object> mapUpdatedFields = new HashMap<String, Object>();
for (Field field : fields) {
field.setAccessible(true);
mapUpdatedFields.put(field.getName(), field.get(t));
}
T newT = this.getById(mapUpdatedFields.get("id").toString(), t);
mapUpdatedFields.remove("id");
mapUpdatedFields.forEach((k, v) -> {
if (!AccountingMethodUtils.isObjectisNullOrEmpty("update()", v)) {
AccountingMethodUtils.setValueToField(newT, k, v);
}
});
entityManager.merge(newT);
} catch (Exception e) {
e.printStackTrace();
LOGGER.error(
"update() of DAO : (Error in outer try..catch) Error in updating record of {} and Error is {}.",
t.getClass(), e);
}
}
#Override
public <T> List<T> executeQuery(String query, Integer startPosition, Integer noOfRecords, T t) {
List<T> listOfT = new ArrayList<>();
if (AccountingMethodUtils.isObjectisNullOrEmpty(startPosition, noOfRecords)) {
listOfT = entityManager.createNativeQuery(query.toString(), t.getClass()).getResultList();
} else {
listOfT = entityManager.createNativeQuery(query.toString(), t.getClass()).setFirstResult(startPosition)
.setMaxResults(noOfRecords).getResultList();
}
return AccountingMethodUtils.isListIsNullOrEmpty(listOfT) ? new ArrayList<>() : listOfT;
}
Service Layer:
#Override
#Transactional
public String updatePurchaseOrder(AccountingRequestBody input) {
PurchaseOrder purchaseOrder = AccountingMethodUtils.getObjectMapper().convertValue(input.getJsonOfObject(),
PurchaseOrder.class);
// Query : db.purchase_order.find( {'_id' : ObjectId("5ab88323191bb91e78f9e33d") } , { 'purchaseOrderNo' : 1, 'currencyMaster_CURRENCY_ID' : 1 , 'total' : 1 })
StringBuilder sb = new StringBuilder().append("db.").append(AccountingVariableUtils.TABLE_NAME_FOR_PURCHASE_ORDER)
.append(".find( {'_id' : ObjectId('" + purchaseOrder.getId().toString()
+ "') } , { 'purchaseOrderNo' : 1, 'currencyMaster_CURRENCY_ID' : 1 , 'total' : 1 })");
List<PurchaseOrder> poFromDB = purchaseOrderDao.executeQuery(sb.toString(), null, null, new PurchaseOrder());
if (!AccountingMethodUtils.isListIsNullOrEmpty(poFromDB)) {
System.out.println("id before update : " + poFromDB.get(0).getCurrencyMaster().getId()); // Output: 5ab8830b191bb91e78f9e221
System.out.println("code before update : " + poFromDB.get(0).getCurrencyMaster().getCode()); // Output: INR
}
purchaseOrderDao.update(purchaseOrder);
poFromDB = purchaseOrderDao.executeQuery(sb.toString(), null, null, new PurchaseOrder());
if (!AccountingMethodUtils.isListIsNullOrEmpty(poFromDB)) {
System.out.println("id after update : " + poFromDB.get(0).getCurrencyMaster().getId()); // Output: 5ab8830b191bb91e78f9e221
System.out.println("code after update : " + poFromDB.get(0).getCurrencyMaster().getCode()); // Output: null //?????????????????????
}
}
output:
id before update : 5ab8830b191bb91e78f9e221
code before update: INR
id after update : 5ab8830b191bb91e78f9e221
code afterupdate: null ?????????????????????
Description:
Currency Master has one to many mapping(bidirectional) with purchase order.
In Service Layer,
first i executed query to get "id,code,total" from purchase order and successfully got all the fields.
then i updated purchase order.
then i again executed same query ( to get "id,code,total" from purchase order) after update then i can get id of currency master but can't get code of currency master.
Is Am i Right???
I think the problem is that you are using projection in your native query and trying to create the entity without retrieving all the information needed. I would remove the projection part from the native query and only use:
db.purchase_order.find( {'_id' : ObjectId("5ab88323191bb91e78f9e33d") } );
This happens because Hibernate OGM caches results, so you have to make sure to initialize entities correctly.
Or, even better, why don't you try using a JPQL query?
PurchaseOrder order = em.createQuery( "FROM PurchaseOrder po WHERE po.id = :id", PurchaseOrder.class )
.setParameter( "id", "5ab88323191bb91e78f9e33d" )
.getSingleResult();
By the way, Hibernate OGM shouldn't let you do this kind of things and will throw an exception in follow-up versions.
Last, I would recommend to update to Hibernate OGM 5.3.0.Final or at least to 5.2.0.Final (if there is any reason for you to stick to stick to the 5.2 family).

Neo4J, Spring Data. How to query Relationship entity?

I use Neo4J database with Spring Data. I am unable to query (with custom query) a relationship directly to my Relation entity which looks like that:
#RelationshipEntity(type = "OCCURS_WITH")
public class Relation {
#GraphId
private Long id;
#StartNode
#Fetch
private Hashtag from;
#EndNode
#Fetch
private Hashtag to;
#GraphProperty(propertyType = long.class)
private Long[] timestamps = new Long[0];
private boolean active;
// getters, setters
}
I have also a repository interface as follow:
public interface RelationRepository extends CRUDRepository<Relation> {
#Query(value = " MATCH (h1)-[rel]->(h2) " +
" WHERE h1.name = {0} AND h2.name = {1}" +
" RETURN rel")
Relation find(String from, String to);
}
But when I query the repository I get an empty Relation object.
Everything works well when I am quering to dummy object in that way:
#Query(value = " MATCH (h1)-[r]->(h2) " +
" WHERE h1.name = {0} AND h2.name = {1} " +
" RETURN id(r) AS id, h1.name AS from, h2.name AS to, length(r.timestamps) AS size")
RelationshipData findData(String from, String to);
#QueryResult
public interface RelationshipData {
#ResultColumn("id")
String getId();
#ResultColumn("from")
String getFrom();
#ResultColumn("to")
String getTo();
#ResultColumn("size")
int getSize();
}
Is it possible to query directly to my entity?

JDBCTemplate set nested POJO with BeanPropertyRowMapper

Given the following example POJO's: (Assume Getters and Setters for all properties)
class User {
String user_name;
String display_name;
}
class Message {
String title;
String question;
User user;
}
One can easily query a database (postgres in my case) and populate a list of Message classes using a BeanPropertyRowMapper where the db field matched the property in the POJO: (Assume the DB tables have corresponding fields to the POJO properties).
NamedParameterDatbase.query("SELECT * FROM message", new BeanPropertyRowMapper(Message.class));
I'm wondering - is there a convenient way to construct a single query and / or create a row mapper in such a way to also populate the properties of the inner 'user' POJO within the message.
That is, Some syntatical magic where each result row in the query:
SELECT * FROM message, user WHERE user_id = message_id
Produce a list of Message with the associated User populated
Use Case:
Ultimately, the classes are passed back as a serialised object from a Spring Controller, the classes are nested so that the resulting JSON / XML has a decent structure.
At the moment, this situation is resolved by executing two queries and manually setting the user property of each message in a loop. Useable, but I imagine a more elegant way should be possible.
Update : Solution Used -
Kudos to #Will Keeling for inspiration for the answer with use of the custom row mapper - My solution adds the addition of bean property maps in order to automate the field assignments.
The caveat is structuring the query so that the relevant table names are prefixed (however there is no standard convention to do this so the query is built programatically):
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
The custom row mapper then creates several bean maps and sets their properties based on the prefix of the column: (using meta data to get the column name).
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;
}
Again, this might seem like overkill for a two class join but the IRL use case involves multiple tables with tens of fields.
Perhaps you could pass in a custom RowMapper that could map each row of an aggregate join query (between message and user) to a Message and nested User. Something like this:
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;
}
});
A bit late to the party however I found this when I was googling the same question and I found a different solution that may be favorable for others in the future.
Unfortunately there is not a native way to achieve the nested scenario without making a customer RowMapper. However I will share an easier way to make said custom RowMapper than some of the other solutions here.
Given your scenario you can do the following:
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;
}
}
The key thing to remember with BeanPropertyRowMapper is that you have to follow the naming of your columns and the properties of your class members to the letter with the following exceptions (see Spring Documentation):
column names are aliased exactly
column names with underscores will be converted into "camel" case (ie. MY_COLUMN_WITH_UNDERSCORES == myColumnWithUnderscores)
Spring introduced a new AutoGrowNestedPaths property into the BeanMapper interface.
As long as the SQL query formats the column names with a . separator (as before) then the Row mapper will automatically target inner objects.
With this, I created a new generic row mapper as follows:
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;
}
}
Update: 10/4/2015. I typically don't do any of this rowmapping anymore. You can accomplish selective JSON representation much more elegantly via annotations. See this gist.
I spent the better part of a full day trying to figure this out for my case of 3-layer nested objects and just finally nailed it. Here's my situation:
Accounts (i.e. users) --1tomany--> Roles --1tomany--> views (user is allowed to see)
(These POJO classes are pasted at the very bottom.)
And I wanted the controller to return an object like this:
[ {
"id" : 3,
"email" : "catchall#sdcl.org",
"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" : "catchall#stereolab.com",
"password" : "stereopass",
"org" : "Stereolab",
"role" : {
"id" : 1,
"name" : "USER",
"views" : [ "viewPublicReports", "viewProducts", "orderProducts", "viewOfferings", "viewMyData", "home", "viewMyOrders" ]
}
}, {
"id" : 6,
"email" : "catchall#ukmedschool.com",
"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" ]
}
} ]
A key point is to realize that Spring doesn't just do all this automatically for you. If you just ask it to return an Account item without doing the work of nested objects, you'll merely get:
{
"id" : 6,
"email" : "catchall#ukmedschool.com",
"password" : "ukmedpass",
"org" : "University of Kentucky College of Medicine",
"role" : null
}
So, first, create your 3-table SQL JOIN query and make sure you're getting all the data you need. Here's mine, as it appears in my Controller:
#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;
}
Note that I'm JOINing 3 tables. Now create a RowSetExtractor class to put the nested objects together. The above examples show 2-layer nesting... this one goes a step further and does 3 levels. Note that I'm having to maintain the second-layer object in a map as well.
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());
}
}
And this gives the desired output. POJOs below for reference. Note the #ElementCollection Set views in the Role class. This is what automatically generates the role_views table as referenced in the SQL query. Knowing that table exists, its name and its field names is crucial to getting the SQL query right. It feels wrong to have to know that... it seems like this should be more automagic -- isn't that what Spring is for?... but I couldn't figure out a better way. You've got to do the work manually in this case, as far as I can tell.
#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; };
}
I worked a lot on stuff like this and do not see an elegant way to achieve this without an OR mapper.
Any simple solution based on reflection would heavily rely on the 1:1 (or maybe N:1) relation. Further your columns returned are not qualified by their type, so you cannot say which columns matches which class.
You may get away with spring-data and QueryDSL. I did not dig into them, but I think you need some meta-data for the query that is later used to map back the columns from your database into a proper data structure.
You may also try the new PostgreSQL json support that looks promising.
NestedRowMapper worked for me, the important part is getting the SQL correct. The Message properties shouldn't have the class name in them so the query should look like this:
QUERY:
SELECT title AS "title", question AS "question", user_name AS "user.user_name", display_name AS "user.display_name" FROM message, user WHERE user_id = message_id

Categories

Resources