requery / requery

requery - modern SQL based query & persistence for Java / Kotlin / Android
Apache License 2.0
3.14k stars 247 forks source link

SELECT not populating OneToMany relations #371

Closed colintheshots closed 7 years ago

colintheshots commented 7 years ago

I'm seeing an issue where my db.select() statements are not populating any OneToMany member variables in my model object, but a db.update() is. I tried both with and without using explicit joins. Am I using the library wrong? How do I get the joins to occur when I call db.select()?

It seems that .update() with CascadeAction.DELETE should remove entities that don't exist in the model being passed. Instead, I'm seeing that .update() is joining the missing entities from the OneToMany relationship from the database which .select() should have joined and that none are being deleted. While this makes a good workaround for the above issue, I'm baffled by the behavior.


@Inject
SingleEntityStore<Persistable> db;

     // ...
     CredentialsEntity credentials = db.select(CredentialsEntity.class)
                .where(CredentialsEntity.CREDENTIALS_ID.eq(SINGLE_ROW_ID))
                .get().first();
     // In debugging, none of the Target children are joined at this point to the credentials object
     // I can't seem to find any explicit joins that will work either

     db.update(credentials).toBlocking().value();
     // Now, the Target objects are joined and they exist in the credentials object
     // However, .update() should be a destructive operation that I don't want to use here

@Entity(stateless = true)
@JsonDeserialize(as = CredentialsEntity.class)
public abstract class Credentials implements Persistable {

       @Key
       int parent_id;

       @OneToMany(mappedBy = "credentials", cascade = {CascadeAction.DELETE, CascadeAction.SAVE})
       List<Target> endpoints;
}

@Entity(stateless = true)
@JsonDeserialize(as = TargetEntity.class)
public abstract class Target implements Persistable {

     @Key
     @JsonProperty(value = "target_id")
     String targetId;

     @ManyToOne
     Credentials credentials;
}
npurushe commented 7 years ago

The behavior you're asking for is usually referred to as eager fetching relations. That's not currently supported in the current version. The current version supports lazy fetching which is also the default in most JPA frameworks. So you would simply call getTargets() or whatever the name of the target collection in your Credential class is called to load the relation.

Update is not a query operation but it may need to load some joined entities in order to perform a cascade operation. It should only be used when you have changes in your object you want to persist.

Hope that makes sense.

colintheshots commented 7 years ago

I'm not seeing that the generated getEndpoints function is actually loading the relation at all. That's the reason I bugged this issue. I was able to load the relation as a side-effect if I did an update operation, but I was not able to load the relation with a SELECT followed by an accessor from the Credentials object. Instead, all of my OneToMany relations were empty, 0-sized lists until the very moment when I did an update operation on the Credentials object.

I asked the question because I figured the update operation is not the correct way to access the relation. But the accessors were not working either (using both the latest released version and 41103fe).

npurushe commented 7 years ago

How did you create the entity? if it's been attached through the store (either by querying and returning it as a result, or insert/updating it) then getEndpoints() should load the data.

Also you can refresh any property or relation at anytime through the refresh api e.g. data.refresh(entity, CredentialsEntity.ENDPOINTS) that will also work

colintheshots commented 7 years ago

Yes, I created the entity by doing this. The getEndpoints() function is not loading the data.

SqlCipherDatabaseSource source = new SqlCipherDatabaseSource(app, Models.DEFAULT, DBSetup.DB_NAME, PASSWORD, SCHEMA_VERSION);
source.setTableCreationMode(TableCreationMode.CREATE_NOT_EXISTS);
rxDb = RxSupport.toReactiveStore(
                    new EntityDataStore<>(source.getConfiguration()));

CredentialsEntity credentials = rxDb.select(CredentialsEntity.class)
                .get().toObservable().toBlocking().first();
credentials.getEndpoints(); 
// this returns an empty list even though the database has data here
rxDb.update(credentials).toBlocking().value(); 
// this insane workaround populates the credentials endpoints objects

I'm using blocking observables to test why this code has not been working, but I'm seeing similar results when I subscribe properly.

I should add that the db.refresh() workaround is working for me to refresh entities from OneToMany relationships.

kjetile commented 7 years ago

I am having the same issue. I can see the tables are generated correctly when looking at the generated sqlite database, but when calling select(), the OneToMany relations are not populated. The workaround of calling refresh() worked for me as well, but I was hoping to avoid having to do that.

   Result<AllProduct> productResult = data.select(AllProduct.class).get();
   Iterator<AllProduct> productIterator = productResult.iterator();
   while(productIterator.hasNext()) {
      // iterating through AllProducts works

      // Calling data.refreshAll(allProduct); here causes the ProductDetail OneToMany relation 
      // to be populated.

      List<ProductDetail> details = allProduct.getProductDetails();
      // details is an ObservableList of size 0.

      for (ProductDetail detail : details) {
         // does not enter the loop
         Log.i(TAG, "detail: " + detail.getProductName());
      }
   }

My models AllProduct and ProductDetail are similar to the Credentials and Target classes in the example in the original post.

AllProduct:

@Entity(stateless = true)
@JsonDeserialize(as = AllProductEntity.class)
public interface AllProduct extends Persistable, Parcelable {

    @Key
    @Generated
    int getId();

    String getName();

    @OneToMany(mappedBy = "allProduct", cascade = {CascadeAction.DELETE, CascadeAction.SAVE})
    List<ProductDetail> getProductDetails();
}

ProductDetail:

@Entity(stateless = true)
@JsonDeserialize(as = ProductDetailEntity.class)
public interface ProductDetail extends Persistable, Parcelable {

    @Key
    int getProductTemplateID();

    String getProductName();

    @ManyToOne
    @JsonIgnore
    AllProduct getAllProduct();
}
kjetile commented 7 years ago

It works if I remove stateless = true. However, now when inserting AllProducts with mData.insert(allProductsList); the OneToMany relation in AllProduct is not persisted. I ended up doing this:

public void writeToDatabase(List<AllProduct> allProductsList) {
   for(AllProduct allProduct : allProductsList) {
      insertProductDetails(allProduct.getProductDetails(), allProduct);
      mData.insert(allProduct);
   }
}
private void insertProductDetails(List<ProductDetail> details, AllProduct allProduct) {
   for (ProductDetail detail : details) {
      detail.setAllProduct(allProduct);
      mData.upsert(detail);
   }
}

List<AllProduct> is an ArrayList that is deserialized from json using jackson.

Is this the correct way of inserting a object with a OneToMany relation? Is it possible to have the insert() update the OneToMany relations automatically?

Agraphie commented 7 years ago

Hi,

in case you didn't notice, you can follow the discussion about the collection insertion problem in #388.