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
Related
I have multiple objects in my array using . If I then send this to my Spring Boot backend with axios and output the FormData beforehand, I get the following image. That fits. In the backend, however, I need this list of objects as an entity. In this case, of type List. Do I do that?
Frontend code:
let data = new FormData();
...
data.append("zugeordnet", JSON.stringify(personNamen));
await axios.post("/neuerEintrag", data,...)
React:
Backend:
#PostMapping("/neuerEintrag")
public String neuerEintrag(HttpServletRequest req,#RequestParam("zugeordnet") List<?> zugeordnet,..) {
List<User> userListe = (List<User>) zugeordnet;
for(User inListe : userListe) //ERROR here
{
System.out.println("USER :" + inListe);
}
...
}
java.lang.ClassCastException: class java.lang.String cannot be cast to class com.home.calendar.User.User
UPDATE
For completeness, here is the user entity and the complete method for a new entry.
#PostMapping("/neuerEintrag")
public String neuerEintrag(HttpServletRequest req, #RequestParam("beschreibung") String beschreibung,
#RequestParam("datum") Date datum, #RequestBody List<User> zugeordnet,
#RequestBody List<Freunde> kontaktAuswahl, #RequestParam("neuAlt") String neuAlt,
#RequestParam("kalenderId") int kalenderId) { }
The User Entity:
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
#JsonIgnoreProperties("user")
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "user")
private List<Kalender> kalenderEinträge;
public User() {
super();
// TODO Auto-generated constructor stub
}
public User(String name, List<Kalender> kalenderEinträge) {
super();
this.name = name;
this.kalenderEinträge = kalenderEinträge;
}
public List<Kalender> getKalenderEinträge() {
return kalenderEinträge;
}
[getter/setter]
Spring can't parse an unknown object.
To get it work, I suggest a new class for the "request".
#Data // lombok - this generates getter/setters/equals/hashcode for you
public class NeuerEintragRequest {
private List<User> zugeordnet;
private String beschreibung;
private int kalendarId;
// and your others fields
}
The controller can now use very type-safe objects.
#PostMapping("/neuerEintrag")
public String neuerEintrag(#RequestBody NeuerEintragRequest request) {
for(User user : request.getUserlist()) {
// a logging framework is a lot better. Try to use log4j or slf4j.
log.info("USER: {}", user);
}
...
}
Typescript
Let axios handle the typing and serializing. See this tutorial: https://masteringjs.io/tutorials/axios/post-json
To post all the needed data, you can create a new object.
// no formdata - just send the object
const data = { zugeordnet: personNamen, kalendarId: 123, beschreibung: 'abc' };
await axios.post("/neuerEintrag", data);
You can also create a interface in typescript, but this is going to much for a stackoverflow-answer. Try to learn more about spring and typescript.
Based on question & comments ,
your front end call data.append("zugeordnet", JSON.stringify(personNamen)); is converting your object to List<String> instead of List<User>.
So you can transform this List<String> to List<User> in your postMapping:
#PostMapping("/neuerEintrag")
public String neuerEintrag(HttpServletRequest req,#RequestParam("zugeordnet") List<?> zugeordnet,..) {
ObjectMapper mapper=new ObjectMapper();
for(String str:zugeordnet){
System.out.println(mapper.readValue(str, User.class));
}
...
}
I am trying to insert items in a list in a database as single values in rows with the name of the sender. I am able to send the payload and insert into a single row with the user detailst. How can I loop through the payload sent and insert all the items into individual rows? I have tried to look for examples no luck. So far I can only insert as a single row in the database
this is the payload
{"labsigned":["234568","234567","2345678","2344556","12335677","2345677","234556","234545"]}
My controller
#RequestMapping(path = "/labreport/createrordispatched", method = RequestMethod.POST)
public ResponseEntity<?> createDispatched(#RequestBody Dispatched dispatched){
if(labDashboardService.createDispatched(dispatched)) {
return ResponseEntity.status(HttpStatus.CREATED).body(true);
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(false);
}
My Service
public boolean createDispatched(Dispatched dispatched) {
dispatched.setCreatedBy(getCurrentUserEmail());
dispatched.setDateCreated(System.currentTimeMillis());
Dispatched ticket = new Dispatched(
dispatched.getCreatedBy(),
dispatched.getDateCreated(),
dispatched.getlabsigned()
);
dispatchedRepository.save(ticket);
return false;
}
My Model
#Entity
#Table(name = "DISPATCHED")
public class Dispatched {
private String id;
private String labsigned;
private Long dateCreated;
private String createdBy;
public Dispatched(){}
public Dispatched(String createdBy, Long dateCreated, String labsigned){
this.labsigned = rorlabsigned;
this.dateCreated = dateCreated;
this.createdBy = createdBy;
}
Assuming that you were able to insert all labsigned in the payload into one single row with the code you mentioned in the question, You should iterate dispatched.labsigned and insert one by one as rows to accomplish what you need. And returning false at the end of method createDispatched will always return HttpStatus.BAD_REQUEST even though the records are successfully saved in the DB, so you might need to change it to return true.
public boolean createDispatched(Dispatched dispatched) {
List<Dispatched> newTickets = new ArrayList<>();
dispatched.setCreatedBy(getCurrentUserEmail());
dispatched.setDateCreated(System.currentTimeMillis());
for(String labSigned:dispatched.getlabsigned()){
Dispatched ticket = new Dispatched(
dispatched.getCreatedBy(),
dispatched.getDateCreated(),
labSigned
);
newTickets.add(ticket);
}
dispatchedRepository.saveAll(newTickets);
return true;
}
Just send in a list of those values. Shouldn't have to be wrapped in a named field on an object. Just send it in as a json array like ["234568","234567","2345678","2344556","12335677","2345677","234556","234545"]. In your controller method, body don't pass it in as Dispatched but instead a List and then just loop through those creating a list of Dispatch objects and then using saveAll in the repository passing in the newly created Dispatched list.
Update: Example without actually compiling. Should be good enough for the example. Also using lombok to make it easier to read and a few other updates.
#AllArgsConstructor
#FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
#RestController
public class DispatchController {
DispatchedEntityFactory dispatchedEntityFactory;
DispatchedRepository dispatchedRepository;
#PostMapping("/labreport/createrordispatched")
public ResponseEntity<Boolean> createDispatched(DispatchedRequest dispatchedRequests){
List<DispatchedEntity> dispatchedEntities = dispatchedEntityFactory.creatMultipleFromDispatchRequest(dispatchedRequests);
if(CollectionUtils.isEmpty(dispatchedEntities)) {
return ResponseEntity.badRequest().body(false);
}
dispatchedRepository.saveAll(dispatchedEntities);
return ResponseEntity.ok(true);
}
}
#Value
public class DispatchedRequest {
String id;
List<String>labsigned;
Long dateCreated;
String createdBy;
}
#Entity
#Table(name = "DISPATCHED")
#Data
#FieldDefaults(level = AccessLevel.PRIVATE)
public class DispatchedEntity {
String id;
String labsigned;
Long dateCreated;
String createdBy;
}
#Component
public class DispatchedEntityFactory {
public List<DispatchedEntity> creatMultipleFromDispatchRequest(final DispatchedRequest dispatchedRequest) {
List<DispatchedEntity> dispatchedEntities = new ArrayList<DispatchedEntity>();
for(String labsignature : dispatchedRequest.getLabsigned()) {
DispatchedEntity dispatchedEntity = new DispatchedEntity(dispatchedRequest.getId(),labsignature, dispatchedRequest.getDateCreated(), dispatchedRequest.getCreatedBy());
dispatchedEntities.add(dispatchedEntity);
}
return dispatchedEntities;
}
}
I am stuck with a issue of identify which constraint triggers DataIntegrityViolationException. I have two unique constraints: username and email but I have no luck trying to figure it out.
I have tried to get the root cause exception but I got this message
Unique index or primary key violation: "UK_6DOTKOTT2KJSP8VW4D0M25FB7_INDEX_4 ON PUBLIC.USERS(EMAIL) VALUES ('copeland#yahoo.com', 21)"; SQL statement:
insert into users (id, created_at, updated_at, country, email, last_name, name, password, phone, sex, username) values (null, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [23505-193]
Reading the error I know email constraint triggers the validation but I want to return to the user something like:
{type: ERROR, message: "The email already exist"}
I have read in other post and people handle it looking for a constraint name into the exception(eg, users_unique_username_idx) and display a proper message to the user. But I couldn't get that type of constraint name
Maybe I am missing a configuration. I am using:
Spring Boot 1.5.1.RELEASE, JPA, Hibernate and H2
My application.properties
spring.jpa.generate-ddl=true
User.class:
#Entity(name = "users")
public class User extends BaseEntity {
private static final Logger LOGGER = LoggerFactory.getLogger(User.class);
public enum Sex { MALE, FEMALE }
#Id
#GeneratedValue
private Long id;
#Column(name = "name", length = 100)
#NotNull(message = "error.name.notnull")
private String name;
#Column(name = "lastName", length = 100)
#NotNull(message = "error.lastName.notnull")
private String lastName;
#Column(name = "email", unique = true, length = 100)
#NotNull(message = "error.email.notnull")
private String email;
#Column(name = "username", unique = true, length = 100)
#NotNull(message = "error.username.notnull")
private String username;
#Column(name = "password", length = 100)
#NotNull(message = "error.password.notnull")
#JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
#Enumerated(EnumType.STRING)
private Sex sex;
#Column(name = "phone", length = 50)
private String phone;
#Column(name = "country", length = 100)
#NotNull(message = "error.country.notnull")
private String country;
public User() {}
// Getters and setters
}
ControllerValidationHandler.class
#ControllerAdvice
public class ControllerValidationHandler {
private final Logger LOGGER = LoggerFactory.getLogger(ControllerValidationHandler.class);
#Autowired
private MessageSource msgSource;
private static Map<String, String> constraintCodeMap = new HashMap<String, String>() {
{
put("users_unique_username_idx", "exception.users.duplicate_username");
put("users_unique_email_idx", "exception.users.duplicate_email");
}
};
// This solution I see in another stackoverflow answer but not work
// for me. This is the closest solution to solve my problem that I found
#ResponseStatus(value = HttpStatus.CONFLICT) // 409
#ExceptionHandler(DataIntegrityViolationException.class)
#ResponseBody
public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) {
String rootMsg = ValidationUtil.getRootCause(e).getMessage();
LOGGER.info("rootMessage" + rootMsg);
if (rootMsg != null) {
Optional<Map.Entry<String, String>> entry = constraintCodeMap.entrySet().stream()
.filter((it) -> rootMsg.contains(it.getKey()))
.findAny();
LOGGER.info("Has entries: " + entry.isPresent()); // false
if (entry.isPresent()) {
LOGGER.info("Value: " + entry.get().getValue());
e=new DataIntegrityViolationException(
msgSource.getMessage(entry.get().getValue(), null, LocaleContextHolder.getLocale()));
}
}
return new ErrorInfo(req, e);
}
The response at this moment is:
{"timestamp":1488063801557,"status":500,"error":"Internal Server Error","exception":"org.springframework.dao.DataIntegrityViolationException","message":"could not execute statement; SQL [n/a]; constraint [\"UK_6DOTKOTT2KJSP8VW4D0M25FB7_INDEX_4 ON PUBLIC.USERS(EMAIL) VALUES ('copeland#yahoo.com', 21)\"; SQL statement:\ninsert into users (id, created_at, updated_at, country, email, last_name, name, password, phone, sex, username) values (null, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [23505-193]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement","path":"/users"}
UPDATE
This is my service layer that handle my persistence operations
MysqlService.class
#Service
#Qualifier("mysql")
class MysqlUserService implements UserService {
private UserRepository userRepository;
#Autowired
public MysqlUserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
#Override
public List<User> findAll() {
return userRepository.findAll();
}
#Override
public Page<User> findAll(Pageable pageable) {
return userRepository.findAll(pageable);
}
#Override
public User findOne(Long id) {
return userRepository.findOne(id);
}
#Override
public User store(User user) {
return userRepository.save(user);
}
#Override
public User update(User usr) {
User user = this.validateUser(usr);
return userRepository.save(user);
}
#Override
public void destroy(Long id) {
this.validateUser(id);
userRepository.delete(id);
}
private User validateUser(User usr) {
return validateUser(usr.getId());
}
/**
* Validate that an user exists
*
* #param id of the user
* #return an existing User
*/
private User validateUser(Long id) {
User user = userRepository.findOne(id);
if (user == null) {
throw new UserNotFoundException();
}
return user;
}
}
Update #2
Repo to reproduce the issue https://github.com/LTroya/boot-users. I commented my handler on ValidationExceptionHandler.class in order to see the exception.
Send twice json at Json to test on Readme.md to POST /users/
What you want to do is rather than specify the unique column requirement on the #Column annotation, you can actual define those with names on the #Table annotation that JPA provides to have further control of those constraints.
#Entity
#Table(uniqueConstraints = {
#UniqueConstraint(name = "UC_email", columnNames = { "email" } ),
#UniqueConstraint(name = "UC_username", columnNames = " { "userName" } )
})
There are now two ways for handling the exception:
In the controller
You could elect to place the parsing logic in your controller and simply catch the DataIntegrityException that spring throws and parse it there. Something like the following pseudo code:
public ResponseBody myFancyControllerMethod(...) {
try {
final User user = userService.myFactoryServiceMethod(...);
}
catch ( DataIntegrityException e ) {
// handle exception parsing & setting the appropriate error here
}
}
The ultimate crux with this approach for me is we've moved code to handle persistence problems up two layers rather than the layer immediately above the persistence tier. This means should we have multiple controllers which need to handle this scenario we either find ourselves doing one of the following
Introduce some abstract base controller to place the logic.
Introduce some helper class with static methods we call for reuse.
Cut-n-paste the code - Yes this happens more than we think.
Placing the code in the presentation tier also introduces concerns when you need to share that service with other consumer types that may not be actually returning some type of html view.
This is why I recommend pushing the logic down 1 more level.
In the service
This is a cleaner approach because we push the validation of the constraint handling to the layer above the persistence layer, which is meant to ultimately be where we handle persistence failures. Not only that, our code actually documents the failure conditions and we can elect either to ignore or handle them based on context.
The caveat here is that I'd recommend you create specific exception classes that you throw from your service tier code in order to identify the unique constraint failures and throw those after you have parsed the ConstraintViolationException from Hibernate.
In your web controller, rest controller, or whatever other consumer that is calling into your service, you simply need to catch the appropriate exception class if necessary and branch accordingly. Here's some service pseudo code:
public User myFancyServiceMethod(...) {
try {
// do your stuff here
return userRepository.save( user );
}
catch( ConstraintViolationException e ) {
if ( isExceptionUniqueConstraintFor( "UC_email" ) ) {
throw new EmailAddressAlreadyExistsException();
}
else if ( isExceptionUniqueConstraintFor( "UC_username" ) ) {
throw new UserNameAlreadyExistsException();
}
}
}
You can specify unique constraints separately but you'd need to that on the entity level like
#Entity(name = "users")
#Table(name = "users", uniqueConstraints = {
#UniqueConstraint(name = "users_unique_username_idx", columnNames = "username"),
#UniqueConstraint(name = "users_unique_email_idx", columnNames = "email")
})
public class User extends BaseEntity { ... }
I have a pretty simple mysql record like this:
+------+-------+-----------+
| id | name | password |
+------+-------+-----------+
| 1 | John | d0c91f13f |
+------+-------+-----------+
... ... ...
And here is its hibernate entity; nothing fancy
#Entity
#Table(name = "user", schema = "", catalog = "trade")
public class UserEntity{
private long id;
private String name;
private String password;
#Id
#Column(name = "id")
public long getId(){
return id;
}
public void setId(long id){
this.id = id;
}
#Column(name = "name")
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
#Column(name = "password")
public String getPasswrod(){
return password;
}
public void setPassword(String password){
this.password = password;
}
}
For convenience, I use Gson to parse the entity from json string which front-end passed in.
The json string for the record is like this:
{"id":1, "name":"John", "password":"d0c91f13f"}
then userEntity will be parsed from the json String:
UserEntity userEntity = gson.fromJson(userJson, UserEntity.class);
I can insert or update the user with Session.save(userEntity) and Session.update(userEntity).
If every field is contained in the json string, then things seemed goes as expected.
But when some field, such as password is omitted:
{"id":1, "name":"John Smith"}
which indicated that I should make a partial update and leave the omitted field not modified, things went wrong. Because
the parsing procedure will set password to Null. and update it to the database.
So, is there a solution to partially update the record in this case?
Going through every field and setting fields one by one will be the last option; anything other than that?
Thanks in advance.
If you know a particular entry exists, then fetching the entry before updating would fill the object with the existing values and you'd only change the values your Json provided. This avoids null updates like you describe.
If, however, the entry is new, then whatever is missing in the Json will be passed as null to the database.
1,Suppose you can deserialized srcUserEntity by:
UserEntity srcUserEntity = gson.fromJson(userJson, UserEntity.class);
2, You can leverage spring's BeanUtil's copy properties method.
BeanUtils.copyProperties(srcUserEntity, desUserEntity, SpringBeanUtil.getNullPropertyNames(srcUserEntity));
3, In your Dao layer, just fetch the model from Database first, then update the properties needs to be updated only, lastly update. Refer to codes as below:
Session currentSession = sessionFactory.getCurrentSession();
UserEntity modelInDB = (UserEntity)currentSession.get(UserEntity.class, user.getId());
//Set properties that needs to update in DB, ignore others are null.
BeanUtils.copyProperties(productStatusModelForPatch, modelInDB, SpringBeanUtil.getNullPropertyNames(productStatusModelForPatch));
currentSession.update(modelInDB);
4, for getNullPropertyNames() method, please refer to [How to ignore null values using springframework BeanUtils copyProperties? (Solved)
public static String[] getNullPropertyNames (Object source) {
final BeanWrapper src = new BeanWrapperImpl(source);
java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors();
Set<String> emptyNames = new HashSet<String>();
for(java.beans.PropertyDescriptor pd : pds) {
Object srcValue = src.getPropertyValue(pd.getName());
if (srcValue == null) emptyNames.add(pd.getName());
}
String[] result = new String[emptyNames.size()];
return emptyNames.toArray(result);
}
// then use Spring BeanUtils to copy and ignore null
public static void myCopyProperties(Object, src, Object target) {
BeanUtils.copyProperties(src, target, getNullPropertyNames(src))
}
The title might not be super clear, here the problem
I'm executing an update in this form:
db.poi.update({
_id: ObjectId("50f40cd052187a491707053b"),
"votes.userid": {
"$ne": "50f5460d5218fe9d1e2c7b4f"
}
},
{
$push: {
votes: {
"userid": "50f5460d5218fe9d1e2c7b4f",
"value": 1
}
},
$inc: { "score":1 }
})
To insert a document in an array only if there isn't one with the same userid (workaround because unique indexes don't work on arrays). The code works fine from mongo console. From my application I'm using this:
#Override
public void vote(String id, Vote vote) {
Query query = new Query(Criteria.where("_id").is(id).and("votes.userid").ne(vote.getUserid()));
Update update = new Update().inc("score", vote.getValue()).push("votes", vote);
mongoOperations.updateFirst(query, update, Poi.class);
}
This works fine if as "userid" I use a String that can't be a mongo ObjectId, but if I use the string in the example, the query executed translates like this (from mongosniff):
update flags:0 q:{ _id: ObjectId('50f40cd052187a491707053b'), votes.userid: { $ne: ObjectId('50f5460d5218fe9d1e2c7b4f') } } o:{ $inc: { score: 1 }, $push: { votes: { userid: "50f5460d5218fe9d1e2c7b4f", value: 1 } } }
The string is now an Objectid. Is this a bug? BasicQuery do the same thing. The only other solution I see is to use ObjectId instead of String for all classes ids.
Any thoughts?
UPDATE:
This is the Vote class
public class Vote {
private String userid;
private int value;
}
This is the User class
#Document
public class User {
#Id
private String id;
private String username;
}
This is the class and mongo document where I'm doing this update
#Document
public class MyClass {
#Id
private String id;
#Indexed
private String name;
int score
private Set<Vote>votes = new HashSet<Vote>();
}
As Json
{
"_id" : ObjectId("50f40cd052187a491707053b"),
"name" : "Test",
"score" : 12,
"votes" : [
{
"userid" : "50f5460d5218fe9d1e2c7b4f",
"value" : 1
}
]
}
Userid in votes.userid is pushed as String, but the same String is compared as an ObjectId in the $ne
It seems to me the problem can be described like this: if you use String in your classes in place of an ObjectId, if you want to use those ids as references (no dbrefs) in other documents (and embedded documents), they are pushed as String (it's ok because they are Strings). It's fine because spring data can map them again to objectid, but it's not fine if you do a query like the one I mentioned; the field is converted to an objectid in the comparison (the $ne operator in this case) but is considered as a string in the embedded document. So, to wrap up, in my opinion the $ne operator in this case should consider the field a String.
My solution was to write a custom converter to store the String as an objectid in the documents where the id is a reference
public class VoteWriteConverter implements Converter<Vote, DBObject> {
#Override
public DBObject convert(Vote vote) {
DBObject dbo = new BasicDBObject();
dbo.put("userid", new ObjectId(vote.getUserid()));
dbo.put("value", vote.getValue());
return dbo;
}
}