How to use composite entity in spring-data-cassandra? - spring-data

I am setting up spring-data-cassandra for the first time and have a class like so:
#Table(value = "contact")
public class Contact {
#Id
UUID id;
...
Location Location;
...
public void setLocation(Location location) {
this.location = location;
}
public Location getLocation() {
return location;
}
}
This gives me an error when starting up:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'mySQLTransactionRepository': Invocation of init method failed; nested exception is org.springframework.data.cassandra.mapping.VerifierMappingExceptions: com.foo.backend.core.Location:
Cassandra entities must have the #Table, #Persistent or #PrimaryKeyClass Annotation
....
Coming from a spring-data-jpa background simply annotating Location with #Embeddable has previously been enough. It looks like this doesn't work with spring-data-cassandra. How do I use compound entities with spring-data-cassandra?
Will have to annotate location as #Transient and do some serialization myself? I tried to annotate my class with #Persistent but was getting an error about PrimaryKey missing on Location. I can't comprehend why a primary key would be necessary...

Because of the non-relational details of Cassandra, you are going to find it doesn't work like JPA.
There are no joins in Cassandra, so embedding another table as as an attribute of a table is not allowed.
Embeddable types are not supported at this time. If you would like to elaborate on the feature request, please create a Jira for SDC*.
Thanks.

Related

Java JPA write only ID for nested entity

How can I avoid unnecessary queries to the DB?
I have LoadEntity with two nested entity - CarrierEntity and DriverEntity. Java class:
#Entity
public class LoadEntity {
...
#ManyToOne
#JoinColumn(name="carrier_id", nullable=false)
private CarrierEntity carrierEntity;
#ManyToOne
#JoinColumn(name="driver_id", nullable=false)
private DriverEntity driverEntity;
}
But API send me carrierId and driverId. I make it:
DriverEntity driverEntity = driverService.getDriverEntityById(request.getDriverId());
loadEntity.setDriverEntity(driverEntity);
loadRepository.save(loadEntity);
How can I write only driverId with JPA?
With Spring Data JPA you can always fall back on plain SQL.
Of course, this will side step all the great/annoying logic JPA gives you.
This means you won't get any events and the entities in memory might be out of sync with the database.
For this reason you might also increase the version column, if you are using optimistic locking.
That said you could update a sing field like this:
interface LoadRepository extends CrudRepository<LoadEntity, Long> {
#Query(query="update load_entity set driver_id = :driverId where carrier_id=:carrier_id", nativeQuery=true)
#Modifying
void updateDriverId(Long carrierId, Long driverId);
}
If you just want to avoid the loading of the DriverEntity you may also use JpaRepository.getById

Saving value in Hibernate to another table than entity table

I have two tables bo_operator and hist_bo_operator_password. In bo_operator the id column is foreign key to hist_bo_operator_password and I can have many the same operator_id in hist_bo_operator_password and only one id in bo_operator.
My entity:
#Entity
#Table(name="bo_operator")
public class Operator implements Serializable
and that's how I am getting values from hist_bo_operator_password:
#ElementCollection
#CollectionTable(name="hist_bo_operator_password", joinColumns=#JoinColumn(name="id_operator"))
#Column(name="password")
public List<String> oldPasswords = new ArrayList<String>();
but when I'm trying to get only one value by:
#ElementCollection
#CollectionTable(name="hist_bo_operator_password", joinColumns=#JoinColumn(name="id_operator"))
#Column(name="password")
public String oldPassword;
I'm getting error:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]: Invocation of init method failed; nested exception is org.hibernate.AnnotationException: Illegal attempt to map a non collection as a #OneToMany, #ManyToMany or #CollectionOfElements: local.vlex.operator.model.Operator.oldPassword
and all I want to do is makeing an insert into hist_bo_operator_password by
operator.setOldPassword(oldPassword);. I think the problem is that it doesn't know which password take if there is many values for the same id.
How to achive it?
#Edit
I also tried:
#Table(name="bo_operator")
#SecondaryTable(name = "hist_bo_operator_password",pkJoinColumns=#PrimaryKeyJoinColumn(name="id_operator", referencedColumnName="id"))
I even found ORDER BY so:
#Column(name="password", table="hist_bo_operator_password")
#OrderBy("data_ins")
public String oldPassword;
but seems like there is no #Limit or something like this in JPA and I still have many values to the same id which cause error:
org.hibernate.HibernateException: Duplicate identifier in table for: [local.vlex.operator.model.Operator#1]
As you described at the first sentence you have one-to-many relationship
#ElementCollection
#CollectionTable(name="hist_bo_operator_password", joinColumns=#JoinColumn(name="id_operator"))
#Column(name="password")
#OrderBy("data_ins")
public List<String> oldPasswords = new ArrayList<String>();
Then add necessary getter
Optional<String> getPassword() {
return oldPasswords.size() > 0
? Optional.of(oldPasswords.get(0))
: Optional.empty();
}
And setter
void setPassword(String password) { // or maybe addPassword?
oldPasswords.add(password);
}
Why don't you create entity of hist_bo_operator_password?
Then you could have List of that entities (instead of just String List) and just add another object to List and save entity Operator.

Spring Gemfire entity class id generation

Is it possible to use auto generated id in Spring Data Gemfire?
for example, if I have a class called MyGemfire
#region("myregion")
class MyGemfire{
#Id
#generatedValue????// if it is not possible what method I have to use to generate id in auto increment fashion?
Long id;
String name;
...
}
From a quick look at SimpleGemfireRepository it doesn't look like the repository is generating an ID:
#Override
public <U extends T> U save(U entity) {
ID id = entityInformation.getId(entity).orElseThrow(
() -> newIllegalArgumentException("ID for entity [%s] is required", entity));
template.put(id, entity);
return entity;
}
Also, this question and its answer suggest there is no ID generation in Gemfire itself.
So what you should do is to create your ID yourself. For example, it should be possible to have two constructors one taking an ID and the othe not taking an ID but generating it. A UUID would be the obvious choice. If you are bound to Long values, you probably have to roll your own algorithm.
To make it obvious to Spring Data which constructor to use when loading instances, you can use the #PersistenceConstructor annotation.

Play framework: issues with implementing restful update operation

We're creating RESTFul API based on Play framework 2.1.x which transfers/accepts data in JSON format. Create, read and delete operations were easy to implement but we've got stuck with update operation.
Here are the entities we have:
Event:
#Entity
public class Event extends Model {
#Id
public Long id;
#NotEmpty
public String title;
#OneToOne(cascade = CascadeType.ALL)
public Location location;
#OneToMany(cascade = CascadeType.ALL)
public List<Stage> stages = new LinkedList<Stage>();
...
}
Location:
#Entity
public class Location extends Model {
#Id
public Long id;
#NotEmpty
public String title;
public String address;
...
}
Stage:
#Entity
public class Stage extends Model {
#Id
public Long id;
#NotEmpty
public String title;
public int capacity;
...
}
In our router we have following entry:
PUT /events/:id controllers.Event.updateEvent(id: Long)
updateEvent method in controller looks following way (note: we use Jackson library to map objects to JSON and back):
#BodyParser.Of(BodyParser.Json.class)
public static Result updateEvent(Long id) {
Event event = Event.find.byId(id);
Http.RequestBody requestBody = request().body();
JsonNode jsonNode = requestBody.asJson();
try {
ObjectMapper mapper = new ObjectMapper();
ObjectReader reader = mapper.readerForUpdating(event);
event = reader.readValue(jsonNode);
event.save();
} catch (IOException e) {
e.printStackTrace();
}
return ok();
}
After we've got Event from database, updated its values by reading from JSON with ObjectReader we try to save updated Event and get exception (similar one we get when trying to update list of Stages):
org.h2.jdbc.JdbcSQLException: Unique index or primary key violation: "PRIMARY_KEY_9F ON PUBLIC.LOCATION(ID)"; SQL statement: insert into location (id, title, address) values (?,?,?) [23505-168]
According to H2 logs framework tries to perform insert operation for location and fails as location with specified id already exists. We've investigated further ant it looks like when we get Event from DB, location is not joined because of lazy fetch. Looks like the problem occurs with saving other entities which our Event has relationships with. We've tried to force fetch operation for location by doing following:
Event event = Ebean.find(Event.class).fetch("location").where().eq("id", id).findUnique();
but still when we update this event with ObjectReader's readValue method and save Event we get the same exception.
We've also tried to create separate Event object from JSON and update Event from DB field by field (implemented merge operation by ourselves) and it worked but it looks odd that framework doesn't provide any means of merging and updating entities with data passed from client.
Could someone advise on how to solve this problem ? Any example showing how to implement merge of entity with JSON data coming from client and updating it in storage would be highly appreciated.
You've probably already fixed the error by now, but in case this helps someone else, I'm answering it anyway.
I'm just a beginner with Play Framework as well, only started a few days ago. But I believe when you have in your code:
event.save();
you should be doing instead:
event.update();
The problem here is that you're not inserting a new entity into the database, but in fact just updating the one already there, so you need to use the second method.
You can find more info about this at http://www.playframework.com/documentation/2.0/api/java/play/db/ebean/Model.html

Ecliplselink - #CascadeOnDelete doesn't work with #Customizer

I have two entities. "Price" class has "CalculableValue" stored as SortedMap field.
In order to support sorted map I wrote customizer. After that, it seems #CascadeOnDelete is not working. If I remove CalculableValue instance from map and then save "Price" EclipseLink only updates priceId column to NULL in calculableValues table...
I really want to keep the SortedMap. It helps to avoid lots of routine work for values access on Java level.
Also, there is no back-reference (ManyToOne) defined in the CalculableValue class, it will never be required for application logic, so, wanted to keep it just one way.
Any ideas what is the best way to resolve this issue? I actually have lots of other dependencies like this and pretty much everything is OneToMany relation with values stored in sorted map.
Price.java:
#Entity
#Table(uniqueConstraints={
#UniqueConstraint(columnNames={"symbol", "datestring", "timestring"})
})
#Customizer(CustomDescriptorCustomizer.class)
public class Price extends CommonWithDate
{
...
#CascadeOnDelete
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#MapKeyColumn(name="key")
#JoinColumn(name = "priceId")
private Map<String, CalculatedValue> calculatedValues =
new TreeMap<String, CalculatedValue>();
...
}
public class CustomDescriptorCustomizer implements DescriptorCustomizer
{
#Override
public void customize(ClassDescriptor descriptor) throws Exception
{
DatabaseMapping jpaMapping = descriptor.getMappingByAttribute("calculatedValues");
((ContainerMapping) mapping).useMapClass(TreeMap.class, methodName);
}
}
Your customizer should have no affect on this. It could be because you are using a #JoinColumn instead of using a mappedBy which should normally be used in a #OneToMany.
You can check the mapping in your customizer using, isCascadeOnDeleteSetOnDatabase()
or set it using
mapping.setIsCascadeOnDeleteSetOnDatabase(true)