I am trying to persist an entity incrementally.
Here is an overview of the Entity class
package aop.web.teacher.rmodels;
// Generated 11 Feb, 2011 3:57:41 PM by Hibernate Tools 3.2.2.GA
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.hibernate.validator.constraints.NotEmpty;
/**
* AopTeacherMaster generated by hbm2java
*/
#Entity
#Table(name="aop_teacher_master"
,schema="public"
)
public class AopTeacherMaster implements java.io.Serializable {
private long id;
private AopTeachersDistrictMaster aopTeachersDistrictMasterByCurrDistrict;
private AopInstitutionmaster aopInstitutionmaster;
private AopTeachersDistrictMaster aopTeachersDistrictMasterByPermDistrict;
#NotEmpty(message="Fathers name is mandatory")
private String fathersName;
#NotEmpty
private String currAddLine1;
private String currAddLine21;
private String currAddLine22;
private String currAddLine3;
private String currDevelopmentBlock;
private String currPoliceStation;
private String currCity;
private String currPin;
private String currState;
private String currCountry;
private String permAddLine1;
private String permAddLine21;
private String permAddLine22;
private String permAddLine3;
private String permDevelopmentBlock;
private String permPoliceStation;
private String permCity;
private String permPin;
private String permState;
private String permCountry;
private Date dateOfBirth;
private Character gender;
private Character maritalStatus;
private String religion;
private String caste;
private String landLineNumber;
private String mobileNumber;
private String email;
private String uidNum;
private String bankName;
private String bankBranch;
private String bankAccountNum;
private String gpfNum;
private Set aopTeachersSanctionedPostDetailsForCurrentIncumbentId = new HashSet(0);
private Set aopTeachersSanctionedPostDetailsForFirstIncumbentId = new HashSet(0);
public AopTeacherMaster() {
}
public AopTeacherMaster(long id) {
this.id = id;
}
Now I have a 2 step wizard like process where in the first screen
user enters some of the properties of the entity and the entity gets merged,
In the second step additional or rest of the properties are filled.
I am using spring 3 annotation based controller where I am using the entity class
as the command object.
On the first go entity gets saved with screen one values then I am sending the
reference from merge as command object for second screen.
However, second screen seems to populate the entries there but nullifies the
existing properties which were from the first screen.
Here is the controller code for the same
#RequestMapping(value = "/insertteacher.html", method = RequestMethod.POST)
public
String testEm(#Valid AopTeacherMaster teacher, BindingResult result,
Map model) {
logger.info("Checking Teacher for error");
if (result.hasErrors()) {
logger.info("User data has:" + result.getErrorCount() + " errors!");
// ////////////////////
for (Object object : result.getAllErrors()) {
if (object instanceof FieldError) {
FieldError fieldError = (FieldError) object;
logger.error("Error on field::" + fieldError.getField()
+ " || error type ::" + fieldError.getCode());
}
}
model.put("smessage", "There was an error");
return "teachersmasterInsert";
}
logger.info("Attemped saving!");
teacher=schoolMasterService.add(teacher);//recieved the reference after merge! Will be used for command object in the next screen
model.put("teacher", teacher);//This is the command object for second screen
model.put("smessage", "teacher inserted successfully");
return "teachersmasterInsert2";
// List myList=testDaoService.findAllTeachers();
// for(Teachermaster t:myList){logger.info("Got::"+t.getId());}
}
#RequestMapping(value = "/insertteacher2.html", method = RequestMethod.POST)
public
String testEm2(#Valid AopTeacherMaster teacher, BindingResult result,
Map model) {
logger.info("Checking Teacher for error second insert");
if (result.hasErrors()) {
logger.info("User data has:" + result.getErrorCount() + " errors!");
// ////////////////////
for (Object object : result.getAllErrors()) {
if (object instanceof FieldError) {
FieldError fieldError = (FieldError) object;
logger.error("Error on field::" + fieldError.getField()
+ " || error type ::" + fieldError.getCode());
}
}
model.put("smessage", "There was an error");
return "teachersmasterInsert";
}
logger.info("Attemped saving!");
teacher=schoolMasterService.add(teacher);
model.put("teacher", teacher);
model.put("smessage", "teacher second instance inserted successfully");
return "teachersmasterInsert";
// List myList=testDaoService.findAllTeachers();
// for(Teachermaster t:myList){logger.info("Got::"+t.getId());}
}
Is this the correct way to do it? Otherwise
how can I achieve this incremental save?
Please suggest!
thanks in advance.
The typical way to implement wizards in annotation based controller is to store partially constructed object in the session, and save it only after the last step:
#Controller
// Model attribute with name "aopTeacherMaster" is transparently stored in the session
#SessionAttribute("aopTeacherMaster")
public class TeacherController {
...
#RequestMapping(value = "/insertteacher2.html", method = RequestMethod.POST)
public String testEm2(#Valid AopTeacherMaster teacher, BindingResult result, Map model) {
...
// No need to save teacher here
}
#RequestMapping(value = "/insertteacherLast.html", method = RequestMethod.POST)
public String testEmLast(#Valid AopTeacherMaster teacher, BindingResult result,
Map model, SessionStatus status) {
...
// Save teacher at the last step
teacher=schoolMasterService.add(teacher);
// Remove it from the session
status.setComplete();
}
}
Alternatively, if you really need incremental save for some reason, you can load the current state of the entity from the database and copy fields with data from the model object manually.
Related
I'm trying to update existing entry in parent Entity and I encounter error I can't understand nor resolve.
I have two entities in a simple crud repository - Parent(User) and Children(movie). I am trying to pass a favourite movie to an user. The goal is that the movie doesn't have to be already in database, and the #PostMapping has to accept an user_id and movie name as parameters, other method uses the movie name, goes through the OMDBapi, parses data from json to fields and then gives the user at user_id the movie as a favourite. The PostMapping sort of works, because it gets the user at user_id, the movie is also added, but when the url looks like this - http://localhost:8080/users/2/fight+club the user at user_id 2 gets the movie as his favourite, but the movie gets it's id also as 2, even if it's first movie being added to repository. What I don't understand is why when I try to debug this every line of code is acting as I expect it to do -
wUser(id=2, name=Jan, favouriteMovies=[Movie(id=1, title=Fight Club, plot=An insomniac office worker and a devil-may-care soap maker form an underground fight club that evolves into much more., genre=Drama, director=David Fincher, posterURL=https://m.media-amazon.com/images/M/MV5BNDIzNDU0YzEtYzE5Ni00ZjlkLTk5ZjgtNjM3NWE4YzA3Nzk3XkEyXkFqcGdeQXVyMjUzOTY1NTc#._V1_SX300.jpg)])
but after it passes repository.save(user) line I get redirected to InvocableHandlerMethod class, into doInvoke method, into
return KotlinDetector.isSuspendingFunction(method) ? this.invokeSuspendingFunction(method, this.getBean(), args) : method.invoke(this.getBean(), args);
this line, and after that it's just deep into the rabbit hole. As I am quite an inexperienced in coding in Java, what probably can be deducted, I don't really understand nor can find solution to this problem.
The entities and controller classes below
package com.example.omdbapirest.movie;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Movie {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="movie_id")
private Integer id;
private String title;
private String plot;
private String genre;
private String director;
private String posterURL;
public Movie(String title, String plot, String genre, String director, String posterURL) {
this.title = title;
this.plot = plot;
this.genre = genre;
this.director = director;
this.posterURL = posterURL;
}
}
package com.example.omdbapirest.user;
import com.example.omdbapirest.movie.Movie;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
#Data
#Entity
#NoArgsConstructor
#AllArgsConstructor
public class wUser {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
// #OneToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.DETACH})
#OneToMany(cascade =CascadeType.ALL)
#JoinColumn(name = "movie_id")
private List<Movie> favouriteMovies;
public wUser(String name) {
this.name = name;
}
}
UserController
package com.example.omdbapirest.user;
import com.example.omdbapirest.movie.Movie;
import com.example.omdbapirest.movie.MovieService;
import lombok.RequiredArgsConstructor;
import org.json.simple.parser.ParseException;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
#RestController
#RequestMapping("/users")
#RequiredArgsConstructor
public class UserController {
private final MovieService movieService;
private final UserRepository repository;
private final UserService service;
#GetMapping
public List<wUser> getUsers(){
return repository.findAll();
}
#PostMapping("/{id}/{moviename}")
public void addMovieAsFavorite (#PathVariable (name= "id") int id,
#PathVariable (name="moviename") String moviename)
throws ParseException{
String url = "https://www.omdbapi.com/?t="+moviename+"&apikey=30ccf40c";
wUser user = repository.getById(id);
List<Movie> movies = user.getFavouriteMovies();
List<Movie>moviesToAdd = new ArrayList<>();
Movie movie = movieService.getDataFromOMDBAsMovie(url);
movies.add(movie);
moviesToAdd.addAll(movies);
user.setFavouriteMovies(moviesToAdd);
repository.save(user);
}
}
I'm also adding MovieService class in case there is some error in the JSON parser
package com.example.omdbapirest.movie;
import lombok.RequiredArgsConstructor;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.List;
#Service
#RequiredArgsConstructor
public class MovieService {
private final MovieRepository repository;
public String getJSONFromURL(String strUrl) {
String jsonText = "";
try {
URL url = new URL(strUrl);
InputStream is = url.openStream();
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(is));
String line;
while ((line = bufferedReader.readLine()) != null) {
jsonText += line + "\n";
}
is.close();
bufferedReader.close();
} catch (Exception e) {
e.printStackTrace();
}
return jsonText;
}
public Movie getDataFromOMDBAsMovie(String strURL) throws ParseException {
String json = getJSONFromURL(strURL);
Movie movie = new Movie();
JSONParser parser = new JSONParser();
Object object = parser.parse(json);
JSONObject mainJsonObject = (JSONObject) object;
String title = (String)mainJsonObject.get("Title");
movie.setTitle(title);
String plot = (String)mainJsonObject.get("Plot");
movie.setPlot(plot);
String genre = (String)mainJsonObject.get("Genre");
movie.setGenre(genre);
String director = (String)mainJsonObject.get("Director");
movie.setDirector(director);
String posterURL = (String)mainJsonObject.get("Poster");
movie.setPosterURL(posterURL);
repository.save(movie);
return movie;
}
public Movie addMovie(Movie movie){
return repository.save(movie);
}
}
I tried adding movies to db, reworking the favourite saving class, all to no avail, I was getting different errors when not debuging, including
org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Pole nie może być NULL"MOVIE_ID"(Field cannot be NULL)
NULL not allowed for column "MOVIE_ID"; SQL statement:
update movie set movie_id=null where movie_id=? [23502-214]
and
org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Naruszenie ograniczenia Klucza Głównego lub Indeksu Unikalnego: "PRIMARY KEY ON PUBLIC.MOVIE(MOVIE_ID)(translating to- Unique Index or primary key violated)
( /* key:2 */ 2, 'David Fincher', 'Drama', 'An insomniac office worker and a devil-may-care soap maker form an underground fight club that evolves into much more.', 'https://m.media-amazon.com/images/M/MV5BNDIzNDU0YzEtYzE5Ni00ZjlkLTk5ZjgtNjM3NWE4YzA3Nzk3XkEyXkFqcGdeQXVyMjUzOTY1NTc#._V1_SX300.jpg', 'Fight Club')"
Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.MOVIE(MOVIE_ID) ( /* key:2 */ 2, 'David Fincher', 'Drama', 'An insomniac office worker and a devil-may-care soap maker form an underground fight club that evolves into much more.', 'https://m.media-amazon.com/images/M/MV5BNDIzNDU0YzEtYzE5Ni00ZjlkLTk5ZjgtNjM3NWE4YzA3Nzk3XkEyXkFqcGdeQXVyMjUzOTY1NTc#._V1_SX300.jpg', 'Fight Club')"; SQL statement:
insert into movie (director, genre, plot, posterurl, title, movie_id) values (?, ?, ?, ?, ?, ?) [23505-214]
Both of these errors appear when I try to add another movie to given user, I mean I was able to give all users 1 movie, but never more since it tries to always add the movie with id of the user
Let's focus on the relevant part of your mapping:
public class Movie {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="movie_id")
private Integer id;
}
and
public class wUser {
#OneToMany(cascade =CascadeType.ALL)
#JoinColumn(name = "movie_id")
private List<Movie> favouriteMovies;
}
The id property of Movie is mapped to the table column movie_id by the configuration in the Movie class.
But for the wUser.favouriteMovies you use #JoinColumn to make it use movie_id the join column, i.e. the column in the Movie table that references the wUser.
By this that column is mapped to two completely different values and it seems in your scenario the second one wins.
To fix this simply choose a different column for the join column. user_id might be a good choice.
This sample was extracted from one of the many samples found via Google.
The application returns 3 objects but all attributes are NULL.
The following is the Controller Object.
import java.net.URI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
#RestController
#RequestMapping(path = "/employees")
public class EmployeeController
{
#Autowired
private EmployeeDAO employeeDao;
#GetMapping(path="/", produces = "application/json")
public Employees getEmployees()
{
return employeeDao.getAllEmployees();
}
#PostMapping(path= "/", consumes = "application/json", produces = "application/json")
public ResponseEntity<Object> addEmployee(#RequestBody Employee employee)
{
Integer id = employeeDao.getAllEmployees().getEmployeeList().size() + 1;
employee.setId(id);
employeeDao.addEmployee(employee);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(employee.getId())
.toUri();
return ResponseEntity.created(location).build();
}
}
Here is the Employees object which gets returned via the getEmployees call.
import java.util.ArrayList;
import java.util.List;
public class Employees
{
private List<Employee> employeeList;
public List<Employee> getEmployeeList() {
if(employeeList == null) {
employeeList = new ArrayList<>();
}
return employeeList;
}
public void setEmployeeList(List<Employee> employeeList) {
this.employeeList = employeeList;
}
}
In case you need it, here is the DAO.
import org.springframework.stereotype.Repository;
#Repository
public class EmployeeDAO
{
private static Employees list = new Employees();
static
{
list.getEmployeeList().add(new Employee(11, "Lokesh", "Gupta", "howtodoinjava#gmail.com"));
list.getEmployeeList().add(new Employee(22, "Alex", "Kolenchiskey", "abc#gmail.com"));
list.getEmployeeList().add(new Employee(33, "David", "Kameron", "titanic#gmail.com"));
}
public Employees getAllEmployees()
{
return list;
}
public void addEmployee(Employee employee) {
list.getEmployeeList().add(employee);
}
}
As requested, here is the Employee class.
import javax.annotation.ManagedBean;
import org.springframework.web.context.annotation.ApplicationScope;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
//#ManagedBean
//#ApplicationScope
//#Data
public class Employee {
public Employee() {
}
public Employee(Integer id, String firstName, String lastName, String email) {
super();
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
#Setter #Getter private Integer id;
private String firstName;
private String lastName;
private String email;
//Getters and setters
#Override
public String toString() {
return "Employee [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + ", email=" + email + "]";
}
}
Here is the partial client code....
private static void getEmployees()
{
// HttpHeaders
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(new MediaType[] { MediaType.APPLICATION_JSON }));
// Request to return JSON format
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("my_other_key", "my_other_value");
// HttpEntity<String>: To get result as String.
HttpEntity<String> entity = new HttpEntity<String>(headers);
// RestTemplate
RestTemplate restTemplate = new RestTemplate();
// Send request with GET method, and Headers.
///This works but only prints IDs.
ResponseEntity<String> response = restTemplate.exchange(URL_EMPLOYEES, HttpMethod.GET, entity, String.class);
String result = response.getBody();
System.out.println(result);
HttpEntity<Employees> entity2 = new HttpEntity<Employees>(headers);
ResponseEntity<Employees> eResponse = restTemplate.exchange(URL_EMPLOYEES, HttpMethod.GET, entity2, Employees.class);
List <Employee> eList = eResponse.getBody().getEmployeeList();
for (Employee e : eList)
{
System.out.println(e.toString());
}
}
From the above, the following call works and prints out the 3 IDs.
ResponseEntity<String> response = restTemplate.exchange(URL_EMPLOYEES, HttpMethod.GET, entity, String.class);
String result = response.getBody();
System.out.println(result);
Once the IDs were returned, the next step was to print out all the attributes for each employee using the next set of calls.
ResponseEntity<Employees> eResponse = restTemplate.exchange(URL_EMPLOYEES, HttpMethod.GET, entity, Employees.class);
List <Employee> eList = eResponse.getBody().getEmployeeList();
for (Employee e : eList)
{
System.out.println(e.toString());
}
I placed a break point on the line doing the LIST call and verified the eResponse object has 3 items in the List BUT all 3 items show each of their associated attributes to be NULL.
Since I confirmed the call seems to be working since the IDs are returned in the first call, what command or setup is missing for the 2nd call to allow for the complete objects to be returned?
I think you should remove super(); in the constructor. We use it to access the members of the super class. Your Employee class has no parent class.
Here are the 3 important pieces changed to get this working.
1 - Server Side - nothing really changed with this procedure.
#GetMapping(path="/", produces = "application/json")
public Employees getEmployees()
{
return employeeDao.getAllEmployees();
}
2 - Client side - simplified it.
private static void getEmployees()
{
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(new MediaType[] { MediaType.APPLICATION_JSON }));
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("my_other_key", "my_other_value");
HttpEntity<Employee> entity = new HttpEntity<>(null,headers);
ResponseEntity<String>result = restTemplate.exchange(URL_EMPLOYEES, HttpMethod.GET, entity, String.class);
System.out.println(result);
}
3 - Server Side - this was the key to getting it working.
Originally the Employee.java looked like this.
#Setter #Getter private Integer id;
private String firstName;
private String lastName;
private String email;
Once i change to the following, it worked.
#Setter #Getter private Integer id;
#Setter #Getter private String firstName;
#Setter #Getter private String lastName;
#Setter #Getter private String email;
Still not 100% of what is really going on but it works.
My application seems to work ok if I have 2 records or less in my database, but if I add more data then I got this warning in maven and error in the response:
2019-03-02 21:36:44.642 WARN 14734 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: 503 Service Temporarily Unavailable; nested exception is com.fasterxml.jackson.databind.JsonMappingException: 503 Service Temporarily Unavailable (through reference chain: java.util.ArrayList[2])]
It's actually quite strange, I receive the data and I'm able to output it in a string for instance, but if I return the list then crashes in execution.
My code:
package com.awesome2048.score;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
#RestController
public class ScoreController {
#Autowired
ScoreRepository repository;
#GetMapping("/scores")
public List<Score> fetchScores() throws JsonProcessingException {
List<Score> scores = (List<Score>) repository.findAll();
return scores;
}
}
The entity Score:
package com.awesome2048.score;
import javax.persistence.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.web.client.RestTemplate;
#JsonSerialize(using = ScoreSerializer.class)
#Entity
#Table(name = "scores")
public class Score {
private static final long serialVersionUID = -3009157732242241606L;
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
#Column(name = "name", nullable = false)
private String name;
#Column(name = "score", nullable = false)
private int score;
#Column(name = "ip", nullable = false)
private String ip;
public Score(String name, int score) {
this.name = name;
this.score = score;
}
public Score() {}
public String getName() {
return this.name;
}
public int getScore() {
return this.score;
}
public String getIp() {
return this.ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getCountryCode() {
String endpoint = "http://api.ipinfodb.com/v3/ip-country/?key=62ee2a10303261af0cf55d6eb2c807c8db5e6fa539fe5ba843c341f4062bfaea&ip= " + this.getIp();
RestTemplate restTemplate = new RestTemplate();
String countryCode = restTemplate.getForObject(endpoint, String.class).split(";")[3];
return countryCode;
}
}
I also implemented a custom serializer:
package com.awesome2048.score;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
public class ScoreSerializer extends StdSerializer<Score> {
public ScoreSerializer() {
this(null);
}
public ScoreSerializer(Class<Score> t) {
super(t);
}
#Override
public void serialize(Score score, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("name", score.getName());
jsonGenerator.writeNumberField("score", score.getScore());
jsonGenerator.writeStringField("countryCode", score.getCountryCode());
jsonGenerator.writeEndObject();
}
}
The reason is your get method:
public String getCountryCode() {
String endpoint = "http://api.ipinfodb.com/v3/ip-country/?key=62ee2a10303261af0cf55d6eb2c807c8db5e6fa539fe5ba843c341f4062bfaea&ip= " + this.getIp();
RestTemplate restTemplate = new RestTemplate();
String countryCode = restTemplate.getForObject(endpoint, String.class).split(";")[3];
return countryCode;
}
You are using ipinfodb API which has limitation. Info from their page:
To remain the stabilities of free service, a rate limit of 2 queries
per second applied to our API servers. Any IP querying the API
faster than this rate will temporarily blocked by our firewall. Your
IP will permanently banned if your keep hitting the API server by
ignoring this rate limit.
getter like this is a classical example of side effect in programming. You can not invoke other services, write data on disk in POJO methods which are designed to get/set variable. Instead, try to create batch job which will scan scores table and update required information. This implementation should take care about server limitation 2 requests per second.
i am quite newwbee with spring-data-cassandra and i am facing problems when i try to create one row within a cassandra table.
This is the exception when i try to run the test, setUp method is never executed:
org.springframework.core.convert.ConversionFailedException: **Failed to convert from type [java.util.HashSet<?>] to type [java.lang.String] for value '[unicon.matthews.entity.DataSync#79135a38[**
id=data_sync_id
orgId=identifier
tenantId=_tenand_id
syncDateTime=2017-09-25T13:35:14.153
syncType=all
syncStatus=fully_completed
]]'; nested exception is org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [unicon.matthews.entity.DataSync] to type [java.lang.String]
...
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [unicon.matthews.entity.DataSync] to type [java.lang.String]
at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:324)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:206)
at org.springframework.core.convert.support.CollectionToStringConverter.convert(CollectionToStringConverter.java:71)
at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:37)
... 60 more
This is the test:
#RunWith(SpringRunner.class)
#SpringBootTest(classes = unicon.matthews.oneroster.service.repository.CassandraConfiguration.class)
public class CassandraOrgRepositoryTests {
final String _userName = UUID.randomUUID().toString();
final String _orgName = UUID.randomUUID().toString();
final String _sourceId = UUID.randomUUID().toString();
final String _id = UUID.randomUUID().toString();
final String _api_key = UUID.randomUUID().toString();
final String _api_secret = UUID.randomUUID().toString();
final String _tenant_id = "_tenand_id";
final Status _status = Status.inactive;
final OrgType _org_type = OrgType.school;
final String _org_identifier = UUID.randomUUID().toString();
#ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost().atLeast(Version.parse("3.0"));
#Autowired CassandraOrgRepository repository;
#Before
public void setUp() throws Exception {
repository.deleteAll();
OrgCassandraTable aPojo = new OrgCassandraTable();
aPojo.setTenantId(_tenant_id );
Org.Builder myOrgBuilder = Org.Builder.class.newInstance();
Map<String, String> metadata = new TreeMap<String,String>();
metadata.put("key","value");
Org myOrgPojo = myOrgBuilder.withIdentifier("identifier")
.withDateLastModified(LocalDateTime.now())
.withMetadata(metadata)
.withName(_orgName)
.withSourcedId(_sourceId)
.withStatus(_status)
.withType(_org_type)
.build();
aPojo.setSourcedId(_sourceId);
// active 0,
// inactive 1,
// tobedeleted 2;
aPojo.setStatus("1");
aPojo.setDateLastModified(LocalDateTime.now() );
aPojo.setName(_orgName);
aPojo.setType(_org_type.toString());
aPojo.setIdentifier(_org_identifier);
aPojo.setTenantId(_tenant_id);
// THIS MUST BE THE PROBLEM!
Set<DataSync> _dataSyncSet = new HashSet<DataSync>();
DataSync.Builder _dataSyncBuilder = DataSync.Builder.class.newInstance();
DataSync new_data_sync=_dataSyncBuilder.withId("data_sync_id")
.withOrgId(myOrgPojo.getIdentifier())
.withSyncDateTime(LocalDateTime.now())
.withSyncStatus(DataSync.DataSyncStatus.fully_completed)
.withSyncType(DataSync.DataSyncType.all)
.withTenantId(_tenant_id)
.build();
_dataSyncSet.add(new_data_sync);
aPojo.setDataSyncs(_dataSyncSet);
aPojo.setApiSecret(_api_secret);
aPojo.setApiKey(_api_key);
aPojo.setId(_id);
repository.save(aPojo);
assertTrue(repository.count() > 0);
System.out.println("Created a org with fake data...");
}
#Test
public void testFindbyId() {
Optional<WrapperOrg> loaded = repository.findById(_id);
Assert.assertNotNull(loaded);
Assert.assertEquals("something went wrong...",_id,loaded.get().getId());
}
}
This is the repository:
import java.util.Optional;
import org.springframework.data.cassandra.repository.CassandraRepository;
import org.springframework.data.cassandra.repository.Query;
// this repo must implement something that paginates rows, because ALLOW FILTERING must not be used
public interface CassandraOrgRepository extends CassandraRepository<OrgCassandraTable> {
#Query("SELECT * FROM org WHERE id = ?0")
Optional<WrapperOrg> findById(final String id);
#Query("SELECT * FROM org WHERE api_key = ?0 AND api_secret = ?1 ALLOW FILTERING")
Optional<WrapperOrg> findByApiKeyAndApiSecret(final String apiKey, final String apiSecret);
#Query("SELECT * FROM org WHERE api_key = ?0 ALLOW FILTERING")
Optional<WrapperOrg> findByApiKey(final String apiKey);
}
This is the CassandraConfiguration class that i mention in the test class. I suspect that i will have to do something here:
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cassandra.core.keyspace.CreateKeyspaceSpecification;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.cassandra.config.SchemaAction;
import org.springframework.data.cassandra.config.java.AbstractCassandraConfiguration;
import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories;
#Configuration
#EnableAutoConfiguration
public class CassandraConfiguration {
#Configuration
#EnableCassandraRepositories
static class CassandraConfig extends AbstractCassandraConfiguration {
private static final String KEYSPACE = "example";
#Override
public String getKeyspaceName() {
return KEYSPACE;
}
#Override
public SchemaAction getSchemaAction() {
return SchemaAction.RECREATE_DROP_UNUSED;
}
protected List<CreateKeyspaceSpecification> getKeyspaceCreations() {
List<CreateKeyspaceSpecification> createKeyspaceSpecifications = new ArrayList<>();
createKeyspaceSpecifications.add(getKeySpaceSpecification());
return createKeyspaceSpecifications;
}
// Below method creates KEYSPACE if it doesnt exist.
private CreateKeyspaceSpecification getKeySpaceSpecification() {
CreateKeyspaceSpecification pandaCoopKeyspace = new CreateKeyspaceSpecification();
pandaCoopKeyspace.name(KEYSPACE);
pandaCoopKeyspace.ifNotExists(true)
.createKeyspace();
return pandaCoopKeyspace;
}
#Override
public String getContactPoints() {
return "localhost";
}
#Override
public String[] getEntityBasePackages() {
return new String[] {"unicon.matthews.oneroster.service.repository"};
}
}
}
This is the Entity pojo class:
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Set;
import org.springframework.cassandra.core.PrimaryKeyType;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.cassandra.mapping.CassandraType;
import org.springframework.data.cassandra.mapping.Column;
import org.springframework.data.cassandra.mapping.Indexed;
import org.springframework.data.cassandra.mapping.PrimaryKeyColumn;
import org.springframework.data.cassandra.mapping.Table;
import com.datastax.driver.core.DataType;
import unicon.matthews.entity.DataSync;
import unicon.matthews.oneroster.Org;
import unicon.matthews.oneroster.OrgType;
import unicon.matthews.oneroster.Status;
#Table(value=OrgCassandraTable.tableName)
public class OrgCassandraTable implements Serializable{
#org.springframework.data.annotation.Transient
public static final String tableName = "org";
#PrimaryKeyColumn(name = "id", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
#CassandraType(type = DataType.Name.TEXT)
#Column("id")
private String id;
#Indexed
#CassandraType(type = DataType.Name.TEXT)
#Column("tenant_id")
private String tenantId;
#Indexed
#CassandraType(type = DataType.Name.TEXT)
#Column("api_key")
private String apiKey;
#Indexed
#CassandraType(type = DataType.Name.TEXT)
#Column("api_secret")
private String apiSecret;
#Indexed
#CassandraType(type = DataType.Name.TEXT)
#Column("org_source_id")
private String sourcedId;
#CassandraType(type = DataType.Name.TEXT)
#Column("org_status")
private String status;
#Column("org_metadata")
private Map<String, String> metadata;
#Column("org_dateLastModified")
#LastModifiedDate
private LocalDateTime dateLastModified;
#Column("org_name")
#CassandraType(type = DataType.Name.TEXT)
private String name;
// ojito que esto es un enum
#Column("org_type")
#CassandraType(type = DataType.Name.TEXT)
private String type;
#Column("org_identifier")
#CassandraType(type = DataType.Name.TEXT)
#Indexed
private String identifier;
// THIS FIELD LOOKS TO BE THE PROBLEM!
#Column("org_data_syncs")
#CassandraType(type = DataType.Name.TEXT)
private Set<DataSync> dataSyncs;
public OrgCassandraTable(){
}
This is DataSync class. It belongs to a third party library, i do not have the code. What do am i doing wrong?
public class DataSync implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String orgId;
private String tenantId;
private LocalDateTime syncDateTime;
private DataSync.DataSyncType syncType;
private DataSync.DataSyncStatus syncStatus;
...getters, setters, equals, hashCode, toString methods
}
...
// getters, setters, hashCode, equals, toString methods.
}
Cassandra is a column-oriented store – Spring Data Cassandra maps each domain class to a single table, there are no relations, and there is no (not yet, but might come) support for embedded objects. Embedded objects in the sense of flattening the data structure to the columns of the table the enclosing object maps to.
However, there is support for user-defined types via #UserDefinedType on the object class representing the data structure. Adding #UserDefinedType requires having control over the class/code.
If you want to stick to the class, then you still have an option to serialize the data yourself, e.g., using Jackson and storing the JSON inside a single Cassandra column:
static class DataSyncWriteConverter implements Converter<DataSync, String> {
public String convert(DataSync source) {
try {
return new ObjectMapper().writeValueAsString(source);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
You should be able to work with collection types as well, meaning, that you can persist a Set<DataSync> within a set<varchar> column in Cassandra with this approach.
One last thing: Using 3rd-party classes comes at the risk of changes to the external classes where you don't have control over. Creating an own data structure by replicating all fields and mapping the data to the 3rd-party-class give you control over the lifecycle of changes.
References:
Saving using a registered Spring Converter.
Here's my User:
package models.user;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import play.data.validation.Constraints.MaxLength;
import play.data.validation.Constraints.MinLength;
import play.data.validation.Constraints.Required;
import play.db.ebean.Model;
#Entity
#Table(name = "T_USER")
public class User extends Model {
#Id
public Long id;
#Required
#MaxLength(30)
#MinLength(4)
public String username;
#Required
#MaxLength(30)
#MinLength(4)
public String password;
#ManyToOne(fetch = FetchType.EAGER)
#Column(nullable = false)
public Role role;
public static Finder<Long, User> find = new Finder<Long, User>(Long.class, User.class);
public User() {
}
public User(String username, String password, Role role) {
this.username = username;
this.password = password;
this.role = role;
}
#Override
public String toString() {
return "[Tying to login : ] [" + username + " - " + password + "]";
}
}
In my controller, I want to get a user's role instance, so here's what I did:
public static Result modules(Long id) {
User user = User.find.byId(id);
if ("Super User".equalsIgnoreCase(user.role.name)) {
return ok();
} else {
return forbidden();
}
}
The problem is, user.role.name is null, but user.role.id is correct here, why EBean doesn't help me to fetch role for users ?
I have experienced this problem on different occasions. You could do the following:
First, try to replace your public fields with private ones and the add the appropriate getters and setters (this is a good pattern when using Java anyway).
Second, you can write a little helper for finding/fetching the needed information. So let's say you need to get the user by Id and the do this string check. Then in your User class you can write a method like this:
public static User findById(Long id) {
return Ebean.find(User.class)
.fetch("role")
.where()
.eq("id", id)
.findUnique();
}
After that, just use the method:
public static Result modules(Long id) {
User user = User.findById(id);
if ("Super User".equalsIgnoreCase(user.getRole().getName())) {
return ok();
} else {
return forbidden();
}
}