I need something that seems not so specific but anyway I was unable to come up with nice and sophisticated solution.
Say I have very simple hibernate/jpa entity:
#Entity(name="entity")
public class Type {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#Column(unique = true, nullable = false)
private String name;
#Column(unique = false, nullable = false)
private boolean defaultType;
}
What i need is to somehow annotate defaultType field so only (and exactly) one persisted entity have this value as true. When new entity get persisted with this defaultType as true, the old one (with defaultType=true) entity has to be altered and its defaultType value changed to false. Also if any entity get changed (its defaultType got changed to true), same rule should apply.
As far I know this can be achieved inside business logic (e.g. in DAO layer), with DB trigger or with hibernates interceptor or event (If there is another way, please let me know). I tried with DAO solution but it's kind of bad solution because it can be bypassed and it is really clumsy for such simple operation. DB triggers can not be added with hibernate/jpa annotations (if I am not mistaken) and i am not sure how to make this functionality with hibernate interceptors/events.
So, what is best solution for this problem?
You need use Callback method in JPA, for example PreUpdate or PostUpdate, for instance:
#Entity
#EntityListeners(com.acme.AlertMonitor.class) // set callback method in another class
public class Account {
Long accountId;
Integer balance;
boolean preferred;
#Id
public Long getAccountId() { ... }
...
public Integer getBalance() { ... }
...
#Transient
public boolean isPreferred() { ... }
...
public void deposit(Integer amount) { ... }
public Integer withdraw(Integer amount) throws NSFException {... }
#PreUpdate // callback method in some class
protected void validateCreate() {
if (getBalance() < MIN_REQUIRED_BALANCE)
throw new AccountException("Insufficient balance to open an
account");
}
#PostUpdate // callback method in some class
protected void adjustPreferredStatus() {
preferred =
(getBalance() >= AccountManager.getPreferredStatusLevel());
}
}
// callback method in another class
public class AlertMonitor {
#PreUpdate // callback method in another class
public void updateAccountAlert(Account acct) {
Alerts.sendMarketingInfo(acct.getAccountId(), acct.getBalance());
}
}
Update: About your question, If I undestand what you want, this code may help you:
#Entity(name="entity")
#EntityListeners(com.yourpackage.TypeListner.class)
public class Type {
...
#Column(unique = false, nullable = false)
private boolean defaultType;
}
public class TypeListner {
pivate static Type objectWithTrue = null;
public void init() { // call this method when application is started
List<Type> results = entityManager
.createQuery("from Type", Type.class)
.getResultList();
for(Type type: results) {
if(type.getDefaultType()) {
objectWithTrue = type;
}
}
}
private void changeDefaultType(Type changed) {
if(changed.getDefaultType()) {
if(changed != objectWithTrue && objectWithTrue != null) {
objectWithTrue.setDefaultType(false);
}
objectWithTrue = changed;
}
}
#PostPresist
public void newType(Type changed) {
changeDefaultType(changed);
}
#PostUpdate
public void updateType(Type changed) {
changeDefaultType(changed);
}
#PreRemove
public void removeType(Type changed) {
if(changed.getDefaultType() && objectWithTrue == changed) {
objectWithTrue = null;
}
}
OR
You can use listner #PreUpdate and #PrePresist and every times overwrite all Type objects without store any variable (it isn't so good for perfomance then first example, but more reliable):
#PreUpdate
void updateType(Type changed) {
if(changed.getDefaultType()
List<Type> results = entityManager
.createQuery("from Type", Type.class)
.getResultList();
for(Type type: results) {
if(changed != type && type.getDefaultType()) {
type.setDefaultType(false);
}
}
}
}
Related
I am currently working on an application connected to a MongoDB instance. I am having trouble where the 'id' field of my object is not being returned to me within the application but is being returned as null.
The schema has an 'entity' as defined below:
{
"entity_id": String,
"parent": String,
"relevance": boolean
}
I'm querying the collection using the Java Sync Driver (4.4.1) like so:
try {
Entity testDoc = collection.find(eq("entity_id", entity_id)).first();
if (testDoc != null) {
//add entity to a list
}
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Failed to get Entity", e);
}
For some reason this will give me every field in the object when I query EXCEPT the entity_id. I keep getting this returned as:
entity_id= null
Two things stick out to me. The first being that every other field is a String (originally the Id was a UUID object but I simplified while troubleshooting) and they still return if it's other fields. The second being that there is a whitespace before this null value as if it's being formatted. Other null values return as field=null instead of field= null
I was looking to see if there is some security setting preventing things from being labeled as *_id or *id from being returned but I have found no such instance.
Edit: Here is the Entity Pojo for clarity
public class Entity {
#BsonProperty(value = "entity_id")
private String entityID;
#BsonProperty(value = "parent")
private String parent;
#Deprecated
#BsonProperty(value = "relevance")
private boolean relevance;
public Entity() {}
public Entity(String entityID, String parent, Boolean relevance) {
this.entityID = entityID;
this.parent = parent;
this.relevance = relevance;
}
public String getEntityID() {
return entityID;
}
public void setEntityID(String entityID) {
this.entityID = entityID;
}
public String getParent() {
return parent;
}
public void setParent(String parent) {
this.parent = parent;
}
public boolean isRelevant() {
return relevance;
}
public void relevance(boolean relevance) {
this.relevance = relevance;
}
}
So update for anyone watching this, it appears to have been an issue with my Eclipse IDE.
I reimported the project into IntelliJ Community Edition and rebuilt the Maven project, etc... After doing so, the test cases passed and my entityID returns in the query. Hopefully if anyone else runs into this issue they can do something similar.
I do have two entities that relate each other via a OneToMany-Relation.
Entity 1 is named "Change" and looks like the following
public class Change {
String attribute1;
#Column(name="\"ATTRIBUTE1\"")
public void getAttribute1() {
return this.attribute1;
}
public void setAttribute1(String attribute1) {
this.attribute1 = attribute1;
}
// and 7 more of these....
List<ChangeTask> relatedChangeTasks = new ArrayList<ChangeTask>();
#OneToMany(cascade={PERSIST, MERGE, REFRESH}
#JoinTable(name="CHANGE_CHANGETASK", joinColumns={#JoinColumn(name="CHANGE_ID")}, inverseJoinColumns={#JoinColumn(name="CHANGETASK_ID")})
#JoinColumn(name="\"relatedChangeTask_ID\"" )
public List<ChangeTask> getRelatedChangeTasks() {
return this.relatedChangeTasks;
}
public void setRelatedChangeTasks(List<ChangeTask> relatedChangeTasks) {
this.relatedChangeTasks = relatedChangeTasks;
}
}
Entity 2 is named ChangeTask and extends Change.
public class ChangeTask extends Change {
// some additional attributes...
}
Persisting a new or existing Change record with one ChangeTask added to the "relatedChangeTask" list works just perfect.
Now I have to change the annotation of the 8 attributes from Default to #Lob, so Change now looks like this:
public class Change {
String attribute1;
#Lob
#Column(name="\"ATTRIBUTE1\"")
#Basic(fetch=EAGER)
public String getAttribute1() {
if(fieldHandler != null) {
return (java.lang.String) fieldHandler.readObject(this, "attribute1", attribute1);
}
return attribute1;
}
public void setAttribute1(String attribute1) {
if(fieldHandler != null) {
this.attribute1= (java.lang.String) fieldHandler.writeObject(this, "attribute1", this.attribute1, attribute1);
return;
}
this.attribute1= attribute1;
}
// and 7 more of these....
List<ChangeTask> relatedChangeTasks = new ArrayList<ChangeTask>();
#OneToMany(cascade={PERSIST, MERGE, REFRESH}
#JoinTable(name="CHANGE_CHANGETASK", joinColumns={#JoinColumn(name="CHANGE_ID")}, inverseJoinColumns={#JoinColumn(name="CHANGETASK_ID")})
#JoinColumn(name="\"relatedChangeTask_ID\"" )
public List<ChangeTask> getRelatedChangeTasks() {
return this.relatedChangeTasks;
}
public void setRelatedChangeTasks(List<ChangeTask> relatedChangeTasks) {
this.relatedChangeTasks = relatedChangeTasks;
}
}
Now, when I try to add a given ChangeTask to a Change the persist operation does not fail. But at the end of the Transaction the relation has not been persisted, meaning the relation-table "CHANGE_CHANGETASK" remains empty. When I debug through the whole process, I can see that the list contains one entry before "entityManager.merge()" operation and it still contains one entry after the merge. But it never arrives at the database.
Does anybody have an idea what I'm doing wrong here? As strange as it may sound, it must be something related with the #Lob annotations. As soon as I remove those again from the entity everything works fine.
Thanks in advance.
You wrote
public void getAttribute1() {
That can't be right. I think you mean
public String getAttribute1() {
Additionally you have annotated the setter:
#Column(name="\"ATTRIBUTE1\"")
public void setAttribute1(String attribute1) {
this.attribute1 = attribute1;
}
You have to annotage either the field or the getter.
I am using #CascadeSave to save child object in separate collection.
My Document classes are :
public class FbUserProfile{
#Id
private long id;
#DBRef(lazy=true)
#CascadeSave()
private Set<FacebookFriend> friends;
#DBRef(lazy=true)
#CascadeSave()
private Set<FacebookFriendList> customFriendList;
}
public class FacebookFriend{
#Id
private long id;
private String name;
}
public class FacebookFriendList{
#Id
private long id;
private String name;
private String list_type;
}
I add some object in both friends,customFriendList.
and try to update fbUserProfile object using:
mongoTemplate.save(fbUserProfile);
note: fbUserProfile already exists in db. Now I am updating this
Error Message: Cannot perform cascade save on child object without id set
If I remove #CascadeSave. It works fine for me. How I can Cascade set objects.
I am also using #CascadeSave with other objects. Its working fine but they are not set object.
I found the same tutorials somewhere else: Baeldung's and JavaCodeGeeks (this is the one i've followed)
I've had that same problem, and I could solve it.
It happens when you try to persist a collection. It doesn't matter that the collection's items have the #Id, because the collection itself won't have it. I edited the code in the EventListener's onBeforeConvert to check if the field you're trying to CascadeSave is a collection (in my case a List). If it's a list, you just cycle through it checking each individual item for #Id and saving them.
If it's not a collection you still have to persist them the same way you did before
#Override
public void onBeforeConvert(Object source) {
ReflectionUtils.doWithFields(source.getClass(), new ReflectionUtils.FieldCallback() {
#Override
public void doWith(Field field)
throws IllegalArgumentException, IllegalAccessException {
ReflectionUtils.makeAccessible(field);
if (field.isAnnotationPresent(DBRef.class) && field.isAnnotationPresent(CascadeSave.class)){
final Object fieldValue = field.get(source);
if(fieldValue instanceof List<?>){
for (Object item : (List<?>)fieldValue){
checkNSave(item);
}
}else{
checkNSave(fieldValue);
}
}
}
});
}
private void checkNSave(Object fieldValue){
DbRefFieldCallback callback = new DbRefFieldCallback();
ReflectionUtils.doWithFields(fieldValue.getClass(), callback);
if (!callback.isIdFound()){
throw new MappingException("Oops, something went wrong. Child doesn't have #Id?");
}
mongoOperations.save(fieldValue);
}
The best way to set an ID on the dependent child object is to write a listener class by extending AbstractMongoEventListener class and override the onConvert() method.
public class CustomMongoEventListener extends
AbstractMongoEventListener<Object> {
#Autowired
private MongoOperations mongoOperations;
#Override
public void onBeforeConvert(final Object entity) {
if (entity.id == null || entity.id.isEmpty()) {
entity.id = generateGuid(); //generate random sequence ID
}
public static String generateGuid() {
SecureRandom randomGen = new SecureRandom();
byte[] byteArray = new byte[16];
randomGen.nextBytes(byteArray);
return new Base32().encodeToString(byteArray).substring(0,26);
}
}
Finally register your custom listener in `your configuration file. For annotation approach use the following code to register :
#Bean
public CustomMongoEventListener cascadingMongoEventListener() {
return new CustomMongoEventListener();
}
The above solution works fine incase if you have a list. But we can avoid firing a save query for each element from the list, as it reduces the performance. Here is the solution I have found out of the above code.
#Override
public void onBeforeConvert(BeforeConvertEvent<Object> event) {
Object source = event.getSource();
ReflectionUtils.doWithFields(source.getClass(), new ReflectionUtils.FieldCallback() {
#Override
public void doWith(Field field)
throws IllegalArgumentException, IllegalAccessException {
ReflectionUtils.makeAccessible(field);
if (field.isAnnotationPresent(DBRef.class) && field.isAnnotationPresent(CascadeSave.class)){
final Object fieldValue = field.get(source);
if(fieldValue instanceof List<?>){
for (Object item : (List<?>)fieldValue){
checkNAdd(item);
}
}else{
checkNAdd(fieldValue);
}
mongoOperations.insertAll(documents);
}
}
});
}
private void checkNAdd(Object fieldValue){
DbRefFieldCallback callback = new DbRefFieldCallback();
ReflectionUtils.doWithFields(fieldValue.getClass(), callback);
if (!callback.isIdFound()){
throw new MappingException("Oops, something went wrong. Child doesn't have #Id?");
}
documents.add(fieldValue);
}
Okey I extend the class and it will check if the document is exist if it exist it will update the document else it insert the document:
#Component
class GenericCascadeMongo(
private val mongoTemplate: MongoTemplate
) : AbstractMongoEventListener<Any>() {
override fun onBeforeConvert(event: BeforeConvertEvent<Any?>) {
val source = event.source
?: return
ReflectionUtils.doWithFields(source.javaClass) { field ->
ReflectionUtils.makeAccessible(field)
if (field.isAnnotationPresent(DBRef::class.java) && field.isAnnotationPresent(CascadeSave::class.java)) {
val fieldValue = field[source]
?: return#doWithFields
if (fieldValue is List<*>) {
fieldValue.filterNotNull().forEach {
checkAndSave(it)
}
} else {
checkAndSave(fieldValue)
}
}
}
}
private fun checkAndSave(fieldValue: Any) {
try {
val callback = DbRefFieldCallback(fieldValue)
ReflectionUtils.doWithFields(fieldValue.javaClass, callback)
if (!callback.isIdFound && callback.id == null) {
mongoTemplate.insert(fieldValue)
}
if (callback.id != null) {
val findById = mongoTemplate.exists(Query(Criteria.where(MConst.MONGO_ID).`is`(callback.id)), fieldValue.javaClass)
if (findById) {
mongoTemplate.save(fieldValue)
} else {
mongoTemplate.insert(fieldValue)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private class DbRefFieldCallback(val fieldValue: Any?) : FieldCallback {
var isIdFound = false
private set
var id: String? = null
private set
#Throws(IllegalArgumentException::class, IllegalAccessException::class)
override fun doWith(field: Field) {
ReflectionUtils.makeAccessible(field)
if (field.isAnnotationPresent(Id::class.java)) {
isIdFound = true
id = ReflectionUtils.getField(field, fieldValue)?.toString()
}
}
}
}
I'm experiencing a bug which seems to be related to the memcache.
java.lang.IllegalArgumentException: Key is incomplete.
at com.google.appengine.api.datastore.KeyFactory.keyToString(KeyFactory.java:164)
at com.googlecode.objectify.cache.KeyMemcacheService.stringify(KeyMemcacheService.java:62)
at com.googlecode.objectify.cache.KeyMemcacheService.putAll(KeyMemcacheService.java:91)
at com.googlecode.objectify.cache.EntityMemcache.empty(EntityMemcache.java:319)
at com.googlecode.objectify.cache.CachingAsyncDatastoreService$5.trigger(CachingAsyncDatastoreService.java:445)
at com.googlecode.objectify.cache.TriggerFuture.isDone(TriggerFuture.java:89)
at com.googlecode.objectify.cache.TriggerFuture.get(TriggerFuture.java:104)
at com.googlecode.objectify.impl.ResultAdapter.now(ResultAdapter.java:34)
at com.googlecode.objectify.util.ResultWrapper.translate(ResultWrapper.java:22)
at com.googlecode.objectify.util.ResultWrapper.translate(ResultWrapper.java:10)
at com.googlecode.objectify.util.ResultTranslator.nowUncached(ResultTranslator.java:21)
at com.googlecode.objectify.util.ResultCache.now(ResultCache.java:30)
at com.googlecode.objectify.util.ResultWrapper.translate(ResultWrapper.java:22)
at com.googlecode.objectify.util.ResultWrapper.translate(ResultWrapper.java:10)
at com.googlecode.objectify.util.ResultTranslator.nowUncached(ResultTranslator.java:21)
at com.googlecode.objectify.util.ResultCache.now(ResultCache.java:30)
The objects I'm trying to persist are extending the following, and have
#Parent Key someclass;
public abstract class AbstractVO<T> implements iVO<T> {
private static final Logger log = Logger.getLogger(AbstractVO.class.getName());
#Id
private Long id;
#Index
private Date lastModified;
public Long getId() {
return id;
}
public Date getLastModified() {
return lastModified;
}
public void setId(Long id) {
this.id = id;
}
public void setLastModified(Date lastModified) {
this.lastModified = lastModified;
}
public Key<?> getKey() {
return Key.create(this);
}
#OnSave
public void onSaveFunction(){
setLastModified(new Date());
}
#SuppressWarnings("unchecked")
public T save(){
try {
ofy().save().entity(this).now();
} catch (IllegalArgumentException e){
log.log(Level.SEVERE, "boom, key was incomplete", e);
}
return (T) this;
}
public Result<Void> delete(){
return ofy().delete().entity(this);
}
#SuppressWarnings("unchecked")
public T refresh(){
if (getId() != null){
return (T) ofy().load().entity(this).now();
}
else {
return (T) this;
}
}
Logs are showing that the data required to save the entities are what you'd expect them to be:
parent id:5813790675304448
parent key:ag5zfnRlY2gtZXNzZW5jZXJZCxIHQ29tcGFueRiAgIDAyK2fCAwLEghDYW1wYWlnbhiAgICA0pCXCwwLEgpBY3Rpb25UeXBlGICAgICS5-gJDAsSDFNlbGxlckFjdGlvbhiAgICAyvOpCgw
entity id: null
Have anyone experienced this problem before and how could I resolve it? I've written test cases to attempt to reproduce on my dev servers, but they are all passing. It only appears to be a problem on production.
Edit:
I have removed the cache on the affected entities, which resulted in the saving of the entity taking 10 seconds (I'm guessing this is the timeout?) and high contention making it blow up..
I think i have identified the problem, it's simply contention.. I have rewritten much of my application to verify - I'm in the middle of migration now and will let you know
This was indeed the problem, my resolution was to remove the entity from the hierarchy and add the parents as indexed fields.
This question already has answers here:
JPA EventListener method not called on change to many-to-many collection?
(2 answers)
Closed 3 years ago.
I have a JPA annotated class which contains a collection like so:
#Entity
public class Employee {
#Id
private int id;
#Basic
private String name;
#OneToMany
#JoinTable(name = "ORG", joinColumns = #JoinColumn(name="MINION"),
inverseJoinColumns = #JoinColumn(name="EMP"))
private List<Employee> minions = new ArrayList<Employee>();
#PreUpdate
public void preUpdate(){ ... }
}
What I'm seeing is that if I have a managed Employee entity and I add to it's collection of minions the preUpdate method is not getting invoked. A new row is added to the mapping table in the DB so I know the update is going through. If I change a property directly on the Employee, like name, then preUpdate fires as expected when the transaction is committed.
Is there a way to get PreUpdate to fire when a mapped collection is modified? Or is there some other technique or Hibernate specific annotation for detecting when this happens?
#PreUpdate event is triggered just before database UPDATE operation is executed for the entity in question.
If you're not updating direct properties of Employee, there's no UPDATE to execute for its table and thus #PreUpdate listener is never called. You should have better luck using #PrePersist event which is triggered by "flush" rather than "update".
Maybe a this custom workaround works:
Create a subclass of ArrayList which identifies changes through ActionListener pattern
public class Employee {
....
private List<Employee> minions = createChangeNotifierList();
private List<Employee> createChangeNotifierList() {
ChangeNotifierList<Employee> l = new ChangeNotifierList<Employee>();
l.setActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
preUpdate();
}
});
return l;
}
public void setMinions(List<Employee> l) {
if (!(l instanceof ChangeNotifierList)) {
l = createChangeNotifierList();
preUpdate();
}
this.minions = l;
}
public void preUpdate(){ ... }
}
public class ChangeNotifierList<T> extends ArrayList<T> {
private ActionListener actionListener;
public ChangeNotifierList() {
}
public ChangeNotifierList(List<T> list) {
super.addAll(list);
}
public void setActionListener(ActionListener actionListener) {
this.actionListener = actionListener;
}
public boolean add(T e) {
boolean b = super.add(e);
if (b) {
notifyChange();
}
return b;
}
private void notifyChange() {
actionListener.actionPerformed(null);
}
.....
}
Here is my implementation for Hibernate provider:
http://pastebin.com/8cPB96bZ
Generally you just mark methods that should be called in the case of a dirty collection with #PreCollectionChange annotation.