I have a use case where in I have to save a recursive object
Two classes :
public class Item{
#Id
private Long id;
#Index
private String name;
#Index
private String sku;
#Index
private Long shopId;
#Index
private String imageUrl="";
#Index
private List<Long>optionIds;
private List<Option> options;
}
public class Option{
#Id
private Long id;
#Index
private String name;
#Index
private String sku;
#Index
private Long shopId;
#Index
private String imageUrl="";
#Index
private List<Long>itemIds;
private List<Item> items;
}
I do save the two objects separately in two different tables as well.
For that I need to add #Ignore on the List field in the Item model
and #Ignore on the List field in the Option model.
I now need a complete recursive structure and want to save that in another table.
To do that I was trying a hack by putting #IgnoreSave(IfNull.class) on the List field in the Item model and on the List field in the Option model.
But when I launched the application after doing the above, I got a stackoverflow error. Error being something of the below sort :
java.lang.StackOverflowError
at java.security.AccessController.doPrivileged(Native Method)
at sun.reflect.annotation.AnnotationInvocationHandler.getMemberMethods(AnnotationInvocationHandler.java:284)
at sun.reflect.annotation.AnnotationInvocationHandler.equalsImpl(AnnotationInvocationHandler.java:196)
at sun.reflect.annotation.AnnotationInvocationHandler.invoke(AnnotationInvocationHandler.java:63)
at com.sun.proxy.$Proxy9.equals(Unknown Source)
at java.util.Arrays.equals(Arrays.java:1869)
at com.googlecode.objectify.impl.translate.TypeKey.equals(TypeKey.java:60)
at java.util.concurrent.ConcurrentHashMap.get(ConcurrentHashMap.java:996)
at com.googlecode.objectify.impl.translate.Translators.get(Translators.java:115)
at com.googlecode.objectify.impl.translate.CreateContext.getTranslator(CreateContext.java:27)
at com.googlecode.objectify.impl.KeyMetadata.findKeyFields(KeyMetadata.java:78)
at com.googlecode.objectify.impl.KeyMetadata.<init>(KeyMetadata.java:50)
at com.googlecode.objectify.impl.translate.ClassTranslatorFactory.createEntityClassTranslator(ClassTranslatorFactory.java:64)
I'm stuck now and need help badly. Is there an alternative solution to store the recursive structure via objectify?
It looks like you are trying to save two different entities that reference each other. For that, you should use Key<?> or Ref<?> fields. Recursion is no problem with pointers like this.
By specifying List<Item> and List<Option>, you are telling Objectify that you want to embed these things recursively in a single entity. Objectify does not support recursive embedded classes (at least, not yet).
Related
I am using Objectify to create an entity:
#Entity
public class Collection {
#Id
private String name;
#Index
private List<Long> viewersIds;
//other fields
}
Now I am trying to retrieve the list of Collections which have a particular viewerId, lets say 1. I have tried:
List<Collection> usersCollections = ofy().load().type(Collection.class).filter("viewersIds",1).list();
and
ofy().load().type(Collection.class).filter("viewersIds =",1).list();
and
ofy().load().type(Collection.class).filter("viewersIds ==",1).list();
Getting all Collections works using:
ofy().load().type(Collection.class).list();
What am I doing wrong?
Thank you!
EDIT:
Changing the Colllection object to contain a list of strings viewerIds instead of Long
#Index
private List<String> viewersIds;
And then query it with:
ofy().load().type(Collection.class).filter("viewerIds", value).list();
works. So this could be a solution if the list can be of Strings.
One thing to check is that viewerIds is indeed indexed in the entities that are supposed to be returned: https://console.cloud.google.com/datastore/entities/query
You're looking for 'in'
.filter("<collection> in", value)
I have Question, Like and Hashtag entities. Also there is one to many relationship between Like and Question entities. I am using google cloud endpoints and my problem begins here. In my list method, I return 20 question as json. But for each question object in query I have to check if user is already liked the question and also fetch related hashtags that belongs to the question. How can I do the same operation by key only batch query. Otherwise, I do
ofy().load().type(Like.class)
.filter("questionRef =", questionKey)
.filter("accountRef =", accountKey).first().now();
for each object.
Like entity
#Entity
#Cache
public class Like {
#Id
#Getter
protected Long id;
#Index
#Load
#ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
private Ref<Account> accountRef;
#Index
#Load
#ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
private Ref<Question> questionRef;
#Index
#Getter
protected Date createdAt;
Like() {
}
Like(Key<Account> accountKey) {
this.accountRef = Ref.create(accountKey);
this.createdAt = new Date();
}
}
Hashtag entity
#Entity
#Cache
public class Hashtag implements Model<Hashtag> {
#Id
#Getter
#ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
private Long id;
#Index
#Load
#ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
private Ref<Question> questionRef;
#Index
#Getter
#Setter
private String text;
private Hashtag() {
}
private Hashtag(Builder builder) {
this.questionRef = builder.questionRef;
this.text = builder.text;
}
}
There are several parts to this question.
First, hashtags: Just store hashtags in the Question as an indexed list property. Easy.
Second, likes: There are a couple ways to do this efficiently.
One is to create a Like entity with a natural key of "account:question" (use the stringified websafe key). This way you can do a batch get by key for all the {user,question} tuples. Some will be absent, some will be present. Reasonably efficient if you're only concerned about 20 questions, especially if you #Cache the Like.
Another is to create a separate Relation Index Entity that tracks all the likes of a user and just load those up each time. You can put 5k items in any list property, which means you'll need to juggle multiple entities when a user likes more than 5k things. But it's easy to load them all up with a single ancestor query. The RIE will need to be #Parented by the User.
On a separate note - don't call fields thingRef. It's just a thing. The data in the database is just a key. You can interchange Ref<?>, Key<?>, and the native low-level Key. Type information doesn't belong in database names.
I am not sure if you can change the structure of your entities. If the answer is no, then there is no option other than the approach you have taken.
If yes, I would suggest structuring your Question to include the Like and Hashtag information as well.
#Entity
public class Question {
#Id
private long id;
private Set<Key<Account>> likedBy;
private List<String> hashtags;
}
For a question, you can retrieve all the information in one single query. Then collect all the Account keys and make another datastore query to retrieve all the people who have liked the question using keys as below:
Map<Key<Account>, Account> likedByAccounts = ofy().load().keys(accountKeys);
I'm using Objectify on Google's AppEngine.
I have the following Entity-Model:
#Entity
public class ChallengeEntity {
#Id
private Long id;
#Index
public List<ChallengeParticipant> participants;
}
The Participant (not an entity... should it be one?)
public class ChallengeParticipant {
#Load
public Ref<UserEntity> user;
// ... participant-specific attributes
}
And the User-Entity:
#Entity
public class UserEntity {
#Id
Long id;
#Index
public String email = "";
}
Now how would I find all challenges for a given user-email?
Something along:
ofy().load().type(ChallengeEntity.class).filter("participants.user.email", "test#local.foo")
I am willing to adapt my entity-model to GAE's needs... how may I support this query efficiently and keep a nice model?
Thanks alot
Assuming your list of ChallengeParticipant is reasonably bounded (a few hundred at most) and you aren't at risk of hitting the 1M per-entity size limit, you're probably best leaving it as embedded.
To perform your query, first lookup the person by email, then filter by person:
UserEntity user = // load user (or get the key) by email
ofy().load().type(ChallengeEntity.class).filter("participants.user", user);
Note that you need to #Index the ChallengeParticipant.user field, not the ChallengeEntity.participants list.
Assuming that email is unique for a user, I'd keep ChallengeParticipant as a separate entity and maintain 2 way relationship with ChallangeEntity:
public class ChallengeParticipant {
#Id
String email; // must be able to uniquely identify a user.
List<Ref<ChallengeEntity>> challenges;
// ... participant-specific attributes
}
ChallengeEntity will exist as is but without any #Index
#Entity
public class ChallengeEntity {
#Id
private Long id;
public List<Ref<ChallengeParticipant>> participants;
}
When you want to add a new participant to a challenge, update both entities (Participant & Challenge) in one transaction. As there are no indexes involved, you'll always get consistent results.
Because the Datastore is shared across multiple versions of an application in App Engine, I'm looking into a way for saving only certain properties of an Entity.
Let's say I have the following class in version 1 of my app:
#Entity
public class ThingA {
#Id private Long id;
private String field1;
private String field2;
}
But in version 2, I changed this class to be:
#Entity
public class ThingA {
#Id private Long id;
private String field1;
private String field2;
private String field3;
}
The problem with saving the whole entity is that every time ThingA is saved on version 1 of the application, it sets "field3" to null.
It would be awesome if there's a way to save only certain fields on ThingA instead of the whole entity.
Thanks
I'm going to answer my own question after Googling a little bit more: The Datastore do not support partial updates to an entity. So that's it.
I have an entity called ReferenceForm which contains an AutoPopulatingList of ReferenceItems. It looks like this:
#Entity
public class ReferenceForm implements Serializable{
private static final long serialVersionUID = -5633788166190438576L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
long id;
#lob
private AutoPopulatingList<ReferenceItem> referenceItems;
}
If I add no annotation at all to the AutoPopulatingList, the field type which hibernate creates is varbinary(255). This causes string truncation errors. To work around this, I used the #lob annotation. This felt questionable at the time, but it worked fine. At this point I was just using HSQLDB.
Now the application needs to run against MSSQL. I have generated the schema using Hibernate, and referenceItems ia an image column on the ReferenceForm table. The items themselves are stored in the ReferenceItem table.
Is #lob an appropriate annotation here?.
EDIT: ReferenceItem looks like this:
#Entity
public class ReferenceItem implements Serializable {
private static final long serialVersionUID = -9077063073733429102L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
long id;
private Title title;
private String firstName;
private String surname;
private String positionHeld;
private String institutionCompany;
#Embedded
private Address address;
#Embedded
private Telephone telephone;
private String email;
private boolean existingReference;
private String fileName;
public ReferenceItem() {
}
...getters and setters
}
SECOND EDIT:
Thanks to Willome for suggesting using #OneToMany. In the end, this is what worked.
//from
#lob
private AutoPopulatingList<ReferenceItem> referenceItems;
//to
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<ReferenceItem> referenceItems = new AutoPopulatingList<ReferenceItem>(ReferenceItem.class);
#OneToMany accurately describes the nature of the relationship
Use the interface (List) instead of the implementation when defining the field. See http://docs.jboss.org/hibernate/core/3.3/reference/en/html/collections.html
Define the CascadeType, otherwise this error appears on saving the entity: org.hibernate.TransientObjectException: object references an unsaved transient instance
Make the FetchType EAGER otherwise you cannot load the form in a different transaction: this error appears: failed to lazily initialize a collection of role: ReferenceForm.referenceItems, could not initialize proxy - no Session
You should replace your #Lob annonation with a #OneToMany and replace the AutoPopulatingList with a collection-valued field declared as an interface type (Check out the topic 6.1. Persistent collections on this link http://docs.jboss.org/hibernate/core/3.3/reference/en/html/collections.html.)
//#Lob
#OneToMany(mappedBy = "referenceForm")
private AutoPopulatingList<ReferenceItem> referenceItems; //fail AutoPopulatingList is not an interface
#OneToMany(mappedBy = "referenceForm")
private Set<ReferenceItem> referenceItems; // OK with Set/Collection/List
Thanks to Willome for suggesting using #OneToMany. In the end, this is what worked.
//from
#lob
private AutoPopulatingList<ReferenceItem> referenceItems;
//to
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<ReferenceItem> referenceItems = new AutoPopulatingList<ReferenceItem>(ReferenceItem.class);
#OneToMany accurately describes the nature of the relationship
Use the interface (List) instead of the implementation when defining
the field. See
http://docs.jboss.org/hibernate/core/3.3/reference/en/html/collections.html
Define the CascadeType, otherwise this error appears on saving the
entity: org.hibernate.TransientObjectException: object references an
unsaved transient instance
Make the FetchType EAGER otherwise you
cannot load the form in a different transaction: this error appears:
failed to lazily initialize a collection of role:
ReferenceForm.referenceItems, could not initialize proxy - no Session