ObjectBox (Java): Property query on a relation doesn't work - java

I am having problems with ObjectBox in my Android app. Everything works, except for this issue, so I know my entity classes are OK.
It's an inventory that makes it possible to do inventories of multiple shops (stores). Each InventoryItem is a product that was scanned when doing the inventory. When the inventory is concluded, one or more text files are generated from the data in the box.
One file is generated for each shop, so I have this code to find which distinct shops were inventoried:
Box<InventoryItem> box = app.getBoxStore().boxFor(InventoryItem.class);
long[] shopIds = box.query()
.build()
.property(InventoryItem_.shopId)
.distinct()
.findLongs();
I get the following when this code runs (only relevant part of the stacktrace):
Caused by: java.lang.IllegalArgumentException: Property "shopId" is of type Relation, but we expected a property of type Long in this context
at io.objectbox.query.PropertyQuery.nativeFindLongs(Native Method)
at io.objectbox.query.PropertyQuery$2.call(PropertyQuery.java:213)
at io.objectbox.query.PropertyQuery$2.call(PropertyQuery.java:210)
at io.objectbox.BoxStore.callInReadTx(BoxStore.java:709)
at io.objectbox.BoxStore.callInReadTxWithRetry(BoxStore.java:654)
at io.objectbox.query.Query.callInReadTx(Query.java:273)
at io.objectbox.query.PropertyQuery.findLongs(PropertyQuery.java:210)
at br.com.donadio.inventario.view.ExportDialog$GenerateFilesAsync.doInBackground(ExportDialog.java:132)
at br.com.donadio.inventario.view.ExportDialog$GenerateFilesAsync.doInBackground(ExportDialog.java:104)
at android.os.AsyncTask$2.call(AsyncTask.java:305)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243) 
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133) 
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607) 
at java.lang.Thread.run(Thread.java:761) 
I also tried using .property(InventoryItem_.shop.targetIdProperty) in the query, but it gives the exact same error. I can't find another way to do this, nor a way to fix my code.
I'm using AndroidX (not that it should matter) and my project is correctly setup for it.
MinSdk is 19, MaxSdk is 28, targetSdk is 28.
ObjectBox version is 2.2.0.
Debugging on a device running Android 7.1.1.
These are the relevant entities:
#Entity
public class InventoryItem
{
#Id
public long id;
public String operator;
public ToOne<Area> area;
public long areaId; // expose relationship target ID
public ToOne<Product> product;
public long productId; // expose relationship target ID
public ToOne<Shop> shop;
public long shopId; // expose relationship target ID
public Date timestamp;
// ...
}
#Entity
public class Shop
{
#Id(assignable=true)
public long id;
#Index #Unique
public String name;
#Backlink
public ToMany<InventoryItem> inventoryItems;
// ...
}

After looking a lot for an answer in ObjectBox's GitHub and here, I came up with a solution, based on this answer on the project's GitHub:
List<InventoryItem> items = box.getAll();
ArrayList<Shop> shops = new ArrayList<>();
for (InventoryItem item : items)
{
Shop shop = item.shop.getTarget();
if (!shops.contains(shop))
shops.add(shop);
}
So, we just find everything and then iterate through the list, getting distinct objects in an ArrayList (or I could just get the Id's into a long[]) . Just a handful more lines of code...
Still, I consider that a bug in ObjectBox's implementation, as it's returning a relationship instead of a property on a long attribute. I'm opening an issue.

Workaround I found is to write your query using the object you want the ID from as the starting class.
In your case, that would be :
QueryBuilder<Shop> qb = app.getBoxStore().boxFor(Shop.class).query(); // Start with Shop if what you need are Shop IDs
qb.link(Shop_.inventoryItems); // Filter on working inventoryItems links
long[] shopIds = qb.build()
.property(Shop_.id)
.distinct()
.findLongs(); // Get your IDs
instead of
Box<InventoryItem> box = app.getBoxStore().boxFor(InventoryItem.class);
long[] shopIds = box.query()
.build()
.property(InventoryItem_.shopId)
.distinct()
.findLongs();

Related

How to ensure backwards compatibility when changing data type of attribute DynamoDB

I am attempting to change the data type of an attribute in one of my DDB tables, but because this data is read from and written to, altering the data type of the attribute causes subsequent read failures when reading old records, which look like this:
could not unconvert attribute
DynamoDBMappingException: expected M in value {N: 1000,}
My question is about how I can change the data type of an attribute in my table, and architect the change such that I can still read the Double value that exists in previous records. Here is the class in question:
#DynamoDBTable(tableName = "Sections")
#Data
#EqualsAndHashCode(callSuper = false)
#NoArgsConstructor
#AllArgsConstructor
#Builder
public class SectionRecord {
#DynamoDBHashKey
private String id;
private Map<String, Double> sectionTarget; //previous definition: private Double sectionTarget;
public void setSectionTarget(Double sectionTarget, String key) {
if (this.sectionTarget == null) {
this.sectionTarget = new HashMap<Double, String>();
}
this.sectionTarget.put(key, sectionTarget);
}
public void getSectionTarget(String key) {
return this.sectionTarget.get(key);
}
}
And eventually, I try to read a record like this:
mapper.load(SectionRecord.class, id);
Which is presumably where the issue comes from - I'm trying to read a Double (which exists in the ddb currently) as a map (the changes I've made to the attribute).
I'd love to hear some guidance on how best to architect such a change such that these backwards compatibility issues could be mitigated.
You have to
Create a new attribute with the new type, both in dynamo and in SectionRecord
Your code should be able to read and work with both
deploy it on production and wait for the old data to disappear (or create a custom migration logic)
Delete the old field, the logic can now rely only on the new field
Welcome to dynamo where you don't have DB migrations :(

Exception : io.objectbox.exception.DbDetachedException Even though entity is attached

Using Jackson with Retrofit, I wanted to have list of friends set to friendToMany during Deserialization. As I have gone through with documentation, we have to manually assign that entity to boxstore when assignable=true is set. So, I am doing this way(as shown in code). This approach works only for first item which this code is part of. It doesn't work for element 2 or further.
#Id(assignable = true)
#JsonProperty("_id")
public long id;
#Transient
private List<Friend> friends = null;
#JsonIgnore
#Backlink(to = "demoResponseToOne")
ToMany<Friend> friendToMany;
#JsonProperty("friends")
public void setFriends(
List<Friend> friends)
{
this.friends = friends;
for (Friend friend : friends)
{
MyApplication.getBoxStore().boxFor(Friend.class).attach(friend);
friendToMany.add(friend);
}
}
Exception thrown is : io.objectbox.exception.DbDetachedException: Cannot resolve relation for detached entities, call box.attach(entity) beforehand. at the time of add(friend). I mean this works when this Root element is first item of list.
You need to attach the Box<Friend> to this as well, which owns the ToMany to be modified:
MyApplication.getBoxStore().boxFor(Friend.class).attach(this);
Background: If you are using #Id(assignable = true) you need to take care of some things that ObjectBox would normally do for you. This includes attaching the Box before modifying any ToMany.
Source:
https://docs.objectbox.io/relations#updating-tomany
https://docs.objectbox.io/advanced/object-ids#self-assigned-object-ids

Neo4J OGM Session.load(ID) returns null object for existing ID

I am conducting some Neo4J tests and running into the following peculiar problem. I created a small model which I'm intending to use with OGM. The model has a superclass Entity and a child class Child. They're both in package persistence.model. Entity has the required Long id; with matching getId() getter.
public abstract class Entity {
private Long id;
public Long getId() {
return id;
}
}
#NodeEntity
Child extends Entity {
String name;
public Child() {
}
}
Creating Child objects and persisting them through OGM works fine. I'm basing myself on the examples found in the documentation and using a Neo4jSessionFactory object, which initialises the SessionFactory with the package persistence.model. The resulting database contains objects with proper ID's filled in.
The problem arises when I try to fetch a Child for a given ID. I'm trying it with three methods, using two connection systems (bolt and ogm):
boltSession.run("MATCH (a:Child) WHERE id(a) = {id} RETURN a", parameters("id", childId));
ogmSession.query("MATCH (a:Child) WHERE id(a) = $id RETURN a", params);
ogmSession.load(Child.class, childId, 1);
The first two methods actually return the correct data. The last one returns a null value. The last one, using OGM, has some obvious benefits, and I'd love to be able to use it properly. Can anyone point me in the right direction?
In your test code you are doing a lookup by id of type int.
private int someIdInYourDatabase = 34617;
The internal ids in Neo4j are of type Long.
If you change the type of the id to long or Long then it will work.
private long someIdInYourDatabase = 34617;

SDN4 - Cannot findById with list of id

In old version, SDN3, I can use findById(List id), but after upgrade to SDN4, I cannot use this function again, always return empty.
This is my sample class :
#NodeEntity
public class Right{
#GraphId
Long graphId;
String id; //random generated UUID
String name;
//Properties & Constructor
}
And then I have RightRepository that contain these code :
public interface RightRepository extends GraphRepository<Right> {
List<Right> findById(List<String> id);
}
Instead of use Loop to get per ID, I need to call repository only once, and get the List (without using findAll())
Is SDN4 already not support it? Is there any other solution?
As I post in a comment and after a further investigation, I think that a custom query is the only way to accomplish your requirement at the moment. This works:
#Query("MATCH (n:Right) WHERE n.id IN {rightIds} RETURN n")
List<Right> findRightById(#Param("rightIds") List<String> rightIds);
Hope it helps

How to map entities to existing graph?

I got a graph which is described by the following Cypher expression:
CREATE
(BMW:Brand {name: "BMW", country: "Germany"}),
(X3:Model {name: "X3", acceleration: 7.1, maxSpeed: 227.5, displacement: 1997, consumption: 6}),
(lastGen:Generation {from: 2013}),
(xDrive20i:Modification {name: "xDrive20i", maxSpeed: 210, acceleration: 8.3, consumption: 7.9}),
(X3)-[:MODEL_OF]->(BMW),
(BMW)-[:MODEL]->(X3),
(lastGen)-[:GENERATION_OF]->(X3),
(X3)-[:GENERATION]->(lastGen),
(xDrive20i)-[:MODIFICATION_OF]->(X3),
(X3)-[:MODIFICATION]->(xDrive20i),
(lastGen)-[:MODIFICATION]->(xDrive20i),
(xDrive20i)-[:MODIFICATION_OF]->(lastGen);
I described a java class matching to Brand's data structure:
#NodeEntity
#TypeAlias("Brand")
public class Brand {
#GraphId
private Long id;
#Indexed(indexType = IndexType.FULLTEXT, indexName = "brand_name")
private String name;
private String origin;
private String owner;
#RelatedTo(type = "MODEL", direction = Direction.OUTGOING)
private Set<Model> models;
//getters and setters are ommited
}
and repository:
public interface BrandRepository extends GraphRepository<Brand>{
//method's signatures are ommited
}
When I call brandRepository.count() it returns 1 as I expect. But if I call brandRepository.getOne(2249L) I get an exception:
java.lang.IllegalStateException: No primary SDN label exists .. (i.e one with starting with __TYPE__)
As I understand reading LabelBasedNodeTypeRepresentationStrategy source, a node has to have at least one label with __TYPE__ prefix.
How do I map the entity to the graph given that I may not change the graph structure?
I wouldn't mind implementing my own custom LabelBasedNodeTypeRepresentationStrategy if there is no other way. But in this case could somebody let me know why it is implemented this way (I think it is not accidentally) and how should I bind custom solution to spring-data-neo4j use it?
I use neo4j-2.0.0-M06 and spring-data-neo4j-3.0.0.M1.
SDN adds additional metadata to your graph when you store entities, that metadata is missing in your case.
You can try to add that metadata yourself by calling
neo4jTemplate.postEntityCreation(node, Brand.class);
but that for instance doesn't index your name field (manual legacy index).

Categories