Re: [objectify-appengine] How to describe descriptive attributes in Objectify?

172 views
Skip to first unread message

Jeff Schnitzer

unread,
Nov 4, 2012, 12:11:54 PM11/4/12
to objectify...@googlegroups.com
You can still model an ExercisePatient entity.  It's not necessarily a bad idea, although it can increase the cost and latency of requests to load the extra entity.

Another alternative is to store an @Embed collection of ExercisePatient in either Patient or Exercise.

BTW you do not need to have a bidirectional relationship in order to have a many-to-many relationship.  You can have a unidirectional relationship and index one side.  To fetch the "other" direction you would use a query.  Just saying that this is an option.

For more about Ref<?>s, see the documentation:


Jeff


On Sun, Nov 4, 2012 at 12:04 PM, Anh Luong <luo...@gmail.com> wrote:
I am creating 2 entity classes with many to many relationship

@Entity
@Index
public class Patient {
    
    @Id long ssn;
    String firstName;
    String lastName;
    List<Key<Exercise>> exercises; // Many to many relationship
    ...
}

public class Exercise {

    @Id String id;
    String name;
    List<Key<Patient>> patients = new ArrayList<Key<Patient>>(); // Many to many relationship
    ...
}

Each Exercise can be assigned to many Patients, and each Patient can be assgined many Exercise. The 2 List above take care of that many to many relationship. 

For each of this relationship, there's a set of frequency - "freq", repetives - "reps", and sets = "sets" as shown below.
    int sets;
    int reps;
    int freq;
In a relational-model database, these fields will belong to the many-to-many relationship Exercise-Patient, not entity Exercise nor entity Patient.  I think these attributes are called descriptive attributes in relational database. I don't know how to describe these attributes using Objectify with the above 2 entities. Any suggestion would be appreciated.

Another question, what's the @Load and the Ref<?> for? I read https://groups.google.com/forum/?fromgroups=#!starred/objectify-appengine/k0MLkiLtIl0 and don't quite get the purpose of using these.

Thanks,
AL


Anh Luong

unread,
Nov 7, 2012, 11:00:20 AM11/7/12
to objectify...@googlegroups.com, je...@infohazard.org
Hi Jeff, 

Here's my modified ExercisePatient and Exercise class using @Embed as your advice. I also use @Load and Ref<?> as recommended in the URL you sent:
@Embed
public class ExercisePatient {

    int sets = 0;
    int reps = 0;
    int freq = 0;
    @Load Ref<Patient> patient = null; // unknown at the time it's created, will be updated when a Trainer assign an Exercise to a Patient
    
    public Patient getPatient() {
        return patient.get();
    }
    public void setPatient(Key<Patient> value) { // I think the example in  this - Hiding Ref<?> section "public void setDriver(Person value)", has Patient instead of Key<Patient> as the argument, which causes error because Ref.create only accepts type Key<?>. 
        this.patient = Ref.create(value);
    }
}

@Entity
@Index
public class Exercise {

    @Id String id;
    String name;
    // List<Ref<Patient>> patients = new ArrayList<Ref<Patient>>(); this is deleted because ExercisePatient already Ref<Patient>
    List<ExercisePatient> patientAssignment = new ArrayList<ExercisePatient>();
    ...
}


Can you help me with these questions?
  1. When I create Exercise, Patient, and ExercisePatient, they are not related to each other yet. By this I mean the value of fields in ExercisePatient will be default value, including null for Ref<Patient> patient. Will the @Load in front of the Ref<Patient> patient cause me problem if it's default value is null? I find that @Load will cause the Ref<Patient> to be loaded, and I just worry if it's a null value, will it cause an exception? If it does, how do I work around that?
  2. At one point in my app, a Trainer will assign an Exercise to a Patient. I will load the Exercise ("ofy().load().type(Exercise.class).id(randomExcerciseId).get()"), then add a new entry in the List<ExercisePatient> patientAssignment. Afterwards, I persist the updated Exercise with ofy().save().entity(thisExercise).now(). Is the old entity updated or a new entity is created?
  3. If I follow your advice to have a unidirectional relationship, here's what I do to
    1. find all of the Patient working on a particular Exercise
      1.         long randomExcerciseId = 0;
      2.         Exercise e1 = ofy().load().type(Exercise.class).id(randomExcerciseId).get(); // query a particular Exercise
      3.         List<ExercisePatient> l1 = e1.patientAssignment; // get the list of @Embed ExercisePatient
      4.         for (ExercisePatient ep1: l1) {
      5.             Patient p1 = ep1.getPatient();
      6.             p1.serializePatient(); // print out Patient information
      7.         }
    2. find all of the Exercise assigned to a particular Patient
      1.         long randomPatientId = 0;
      2.         List<Patient> patientsAssignedThisExercise = new ArrayList<Patient>();
      3.         List<Exercise> le1 = ofy().load().type(Exercise.class).list(); // query all Exercise
      4.         for (Exercise e2: le1) {
      5.             List<ExercisePatient> l2 = e2.patientAssignment; // get the list of @Embed ExercisePatient
      6.             for (ExercisePatient ep2: l2) {
      7.                 Patient p1 = ep2.getPatient(); // get the Ref<Patient> from the ExercisePatient entity
      8.                 if (p1.ssn == randomPatientId) { 
      9.                     patientsAssignedThisExercise.add(p1); // if Id match, then add to our list
      10.                     break;
      11.                 }
      12.             }
      13.         }
    3. Do you think there's something can be optimized my code. It still looks a little too long to me to run such a small query.
  4. After all, if I decide to user Ref<?> and later want to convert it back to Key<?>, is there anything I need to change? The doc mentions it can be used interchangeably, but I think there would be some change required, at least to pass the build errors.
Thanks,
AL

Jeff Schnitzer

unread,
Nov 7, 2012, 12:24:17 PM11/7/12
to Anh Luong, objectify...@googlegroups.com
I will try to answer these inline:


On Wed, Nov 7, 2012 at 11:00 AM, Anh Luong <luo...@gmail.com> wrote:
Hi Jeff, 

Here's my modified ExercisePatient and Exercise class using @Embed as your advice. I also use @Load and Ref<?> as recommended in the URL you sent:
@Embed
public class ExercisePatient {

    int sets = 0;
    int reps = 0;
    int freq = 0;
    @Load Ref<Patient> patient = null; // unknown at the time it's created, will be updated when a Trainer assign an Exercise to a Patient
    
    public Patient getPatient() {
        return patient.get();
    }
    public void setPatient(Key<Patient> value) { // I think the example in  this - Hiding Ref<?> section "public void setDriver(Person value)", has Patient instead of Key<Patient> as the argument, which causes error because Ref.create only accepts type Key<?>. 
        this.patient = Ref.create(value);
    }
}


Grab current master code, or download 4.0b1 from maven central (it should be synced within the hour, the ticket was closed 1hr ago).  There is now a Ref.create(pojo) method.

The Objectify home page now directs you on how to download the (officially released) version.

 
@Entity
@Index
public class Exercise {

    @Id String id;
    String name;
    // List<Ref<Patient>> patients = new ArrayList<Ref<Patient>>(); this is deleted because ExercisePatient already Ref<Patient>
    List<ExercisePatient> patientAssignment = new ArrayList<ExercisePatient>();
    ...
}


Can you help me with these questions?
  1. When I create Exercise, Patient, and ExercisePatient, they are not related to each other yet. By this I mean the value of fields in ExercisePatient will be default value, including null for Ref<Patient> patient. Will the @Load in front of the Ref<Patient> patient cause me problem if it's default value is null? I find that @Load will cause the Ref<Patient> to be loaded, and I just worry if it's a null value, will it cause an exception? If it does, how do I work around that?

Null is fine, this is by design.  There are several states that a Ref field can have:

 * The field can be null, which is just that - the field is null.  @Load understands not to try to load.
 * It can hold a reference to a real entity, @Load-ed.  The native datatype is Key and you can get this entity by calling Ref.get().
 * It can hold a reference to an entity that does not exist, @Load-ed.  The native datatype is Key bug when you call Ref.get() you will get null.
 * It can hold a reference but not be @Load-ed.  The native datatype is Key but calling Ref.get() will produce an exception.

In practice you probably don't want to have references to nonexistent entities in your data model, so you won't run across that case.  But at least you can detect it.
 
  1. At one point in my app, a Trainer will assign an Exercise to a Patient. I will load the Exercise ("ofy().load().type(Exercise.class).id(randomExcerciseId).get()"), then add a new entry in the List<ExercisePatient> patientAssignment. Afterwards, I persist the updated Exercise with ofy().save().entity(thisExercise).now(). Is the old entity updated or a new entity is created?

Saving an entity with the same id/parent fields (ie, the same key) always overwrites the previous entity with that key.  If you change the id or parent field, you will save a new entity.  Keys define the identity of an entity.

Also, this sounds like a bad design.  Presumably you will have thousands or even millions of patients.  Collection fields in GAE can have at most 5k entries... and even before you get there you are likely to hit the 1MB size limit of a single entity.  And pushing large entities through the system slows things down in general.

Flip it around so that Patient has the ExercisePatient relationship.  Any given patient probably only has a dozen or so exercises, right?
 
  1. If I follow your advice to have a unidirectional relationship, here's what I do to
    1. find all of the Patient working on a particular Exercise
      1.         long randomExcerciseId = 0;
      2.         Exercise e1 = ofy().load().type(Exercise.class).id(randomExcerciseId).get(); // query a particular Exercise
      3.         List<ExercisePatient> l1 = e1.patientAssignment; // get the list of @Embed ExercisePatient
      4.         for (ExercisePatient ep1: l1) {
      5.             Patient p1 = ep1.getPatient();
      6.             p1.serializePatient(); // print out Patient information
      7.         }

As I said, flip it around.  Presuming you have something like this:

@Embed
public class PatientExercise {
    @Index @Load Ref<Patient> patient;
    @Index @Load Ref<Exercise> exercise;
    String someOtherData;

@Entity
public class Patient {
    @Id Long id;
    List<PatientExercise> exercises = Lists.newArrayList();
}

Then you can get all the patients for a particular exercise:

List<Patient> doers = ofy().load().type(Patient.class).filter("exercises.exercise", theExercise).list();

    1. find all of the Exercise assigned to a particular Patient
      1.         long randomPatientId = 0;
      2.         List<Patient> patientsAssignedThisExercise = new ArrayList<Patient>();
      3.         List<Exercise> le1 = ofy().load().type(Exercise.class).list(); // query all Exercise
      4.         for (Exercise e2: le1) {
      5.             List<ExercisePatient> l2 = e2.patientAssignment; // get the list of @Embed ExercisePatient
      6.             for (ExercisePatient ep2: l2) {
      7.                 Patient p1 = ep2.getPatient(); // get the Ref<Patient> from the ExercisePatient entity
      8.                 if (p1.ssn == randomPatientId) { 
      9.                     patientsAssignedThisExercise.add(p1); // if Id match, then add to our list
      10.                     break;
      11.                 }
      12.             }
      13.         }

This is easier... just load the Patient and look at the 'exercises' collection.
 
    1. Do you think there's something can be optimized my code. It still looks a little too long to me to run such a small query.
  1. After all, if I decide to user Ref<?> and later want to convert it back to Key<?>, is there anything I need to change? The doc mentions it can be used interchangeably, but I think there would be some change required, at least to pass the build errors.

You can swap the type of a field at will between Ref<?>, Key<?>, and native datastore Key.  Depending on how you have encapsulated the field you may need to change your java client code, but Objectify doesn't care which you use.

Jeff

Anh Luong

unread,
Nov 7, 2012, 4:06:58 PM11/7/12
to objectify...@googlegroups.com, Anh Luong, je...@infohazard.org
Jeff,  I cannot find 4.0b1 here . Is that the correct link?

You are totally right about using the List<Exercise> instead of List<Patient>, thanks for saving me from a lot of problems. 1 question about the PatientExercise class: Is the Ref<Patient> patient redundant here? Each Patient has a List<PatientExercise> exercises, then I can query Exercise assigned to a Patient easily, and I can also query Patient working on a particular Exercise by searching through each "Ref<Exercise> exercise" in "List<PatientExercise> exercises" of each Patient. Will the addition of @Index @Load Ref<Patient> patient improve my searching algorithm? Did I miss something?

@Embed
public class PatientExercise {
    @Index @Load Ref<Patient> patient;
    @Index @Load Ref<Exercise> exercise;
    String someOtherData;

@Entity
public class Patient {
    @Id Long id;
    List<PatientExercise> exercises = Lists.newArrayList();
}

Thanks,
AL

Jeff Schnitzer

unread,
Nov 7, 2012, 9:52:05 PM11/7/12
to objectify...@googlegroups.com
No, that link is for historical releases only.  Look at https://code.google.com/p/objectify-appengine/wiki/MavenRepository, which has a link to Maven Central.  It just synced up today, so 4.0b1 is officially released - I will send an announcement in a separate email.

Ooops!  Just eliminate that "@Index @Load Ref<Patient> patient" from PatientExercise.  I must have been distracted.  Sorry.

Jeff

Anh Luong

unread,
Dec 5, 2012, 10:15:29 AM12/5/12
to objectify...@googlegroups.com, je...@infohazard.org
Hi Jeff,

I was able to got my project up and running with the above classes. I added a couple new fields to let the Patient and Therapist comment on a particular Exercise by adding a couple fields to PatientExercise as below:

@Embed
public class PatientExercise {
    
    int sets = 0;
    int reps = 0;
    int freq = 0;
    int status = Constants.PATIENT_EXERCISE_STATUS_ACTIVE; // 0=active; 1=stopped
    @Load Ref<Exercise> exercise = null; // Exercise is an @Entity
    @Load Ref<Therapist> assignTherapist = null; // Therapist who trains this Exercise. Therapist is an @Entity
    String patientComment = null;
    String therapistComment = null;
}

@Entity
@Index
public class Patient {
    
    int age;
    String occupation;
    
    List<String> injuredPart = new ArrayList<String>();
    @Load Ref<Therapist> currentTherapist; // LIMIT: trained by only 1 therapist at a time. Many to one relationship
    List<PatientExercise> assignedExercise = new ArrayList<PatientExercise>(); // Many to many relationship
}    

This works fine if Patient and Therapist leave only 1 comment (the highlighted fields above). If I want to capture all comments they leave and display them later in time line, I must associate each of them with Date. For example. I want to add a couple Lists of ExerciseComment to the PatientExercise class as below:

    List<ExerciseComment> patientCommentList = new ArrayList<PatientComment>();
    List<ExerciseComment> patientCommentList = new ArrayList<PatientComment>();

@Embed
public class ExerciseComment {

    String comment;
    Date date;
}

Per warning described in Objectify http://code.google.com/p/objectify-appengine/wiki/Entities#Embedded_Collections_and_Arrays , this won't work because this is a nested @Embed collection (ExerciseComment in PatientExercise) inside a @Embed collection (PatientExercise in Patient).

My current solution is adding 2 more classes: PatientExerciseComment for Patient's comment and TherapistPatientExerciseComment for THerapist's comment, then add 2 List under Patient as shown below:

@Embed
public class PatientExerciseComment {

    @Load Ref<Exercise> exercise = null; // Exercise is an @Entity
    @Load Ref<Therapist> reviewee = null; // Therapist who trains this Exercise. Therapist is an @Entity
    String comment = null;
    Date date;
}


@Embed
public class TherapistPatientExerciseComment {

    @Load Ref<Exercise> exercise = null; // Exercise is an @Entity
    String comment = null;
    Date date;
}

@Entity
@Index
public class Patient {
    
    int age;
    String occupation;
    
    List<String> injuredPart = new ArrayList<String>();
    @Load Ref<Therapist> currentTherapist; // LIMIT: trained by only 1 therapist at a time. Many to one relationship
    List<PatientExercise> assignedExercise = new ArrayList<PatientExercise>(); // Many to many relationship
    List<PatientExerciseComment> patientCommentList = new ArrayList<PatientExerciseComment>(); // All of Patient comments
    List<TherapistPatientExerciseComment > therapistCommentList = new ArrayList<TherapistPatientExerciseComment >(); // All of Therapist replies
}    

With this implementation, I will do the below steps to display Patient's comments on a particular Exercise
  1. Find all elements in the patientCommentList with Ref<Exercise>'s Id matches that particular Exercise
  2. Sort these based on the Date
And to display Patient and Therapist comments in timeline, I will repeat the above steps for therapistCommentList, then sort of of the combination of 2 List.

I have 2 questions for you:
  1. Do you plan to support nesting @Embed collection inside other @Embed collection in the future? Is that the limit of Google datastore?
  2. Do you have any suggestion on my solution to make it simpler and quicker to retrieve comments from both Therapist and Patient on a particular Exercise?

Thanks,
Anh

Jeff Schnitzer

unread,
Dec 6, 2012, 12:56:03 AM12/6/12
to objectify...@googlegroups.com
On Wed, Dec 5, 2012 at 10:15 AM, Anh Luong <luo...@gmail.com> wrote:

I have 2 questions for you:
  1. Do you plan to support nesting @Embed collection inside other @Embed collection in the future? Is that the limit of Google datastore?

The answer to this is slightly complicated.  @Embed will never allow nested collections; this is a limit of the datastore.  The docs describe how @Embed collections are implemented, which should make it easy to understand why.

However, there is a recent feature in the low level api called EmbeddedEntity.  When Objectify supports this (still thinking about the API) then you will be able to have deeply nested collections.  They won't be indexible, and the annotation won't be @Embed.
 
  2. Do you have any suggestion on my solution to make it simpler and quicker to retrieve comments from both Therapist and Patient on a particular Exercise?

It sounds to me like you're trying to embed too much stuff into an entity.  Any time you have an unbounded number of items, like comments, you probably don't want to use embedding.  Just store each comment as a separate entity with an index on the right fields, then query when you want a list of the comments.

Jeff
Reply all
Reply to author
Forward
0 new messages