0

I'm persisting an entity in a CouchBase repository and trying to query it. The entity looks like this:

@Document(expiry = 0)
public class GsJsonStore implements Serializable {
    private static final long serialVersionUID = 7133072282172062535L;
    @Id @GeneratedValue(strategy=GenerationType.AUTO)
    private long id;
    @Field
    private Map<String,Object> _object;
    @Field
    private String _subject;
    @Field
    private String _predicate;
    //Getters and Setters 
    }

I'm querying the entity by using N1QL queries on the CouchbaseOperations template like this:

String query1 =  "SELECT META(default).id as _ID, META(default).cas as _CAS, default.* FROM default WHERE "+key+"="+"'"+value+"'";

List<GsJsonStore> list = operations.findByN1QL(N1qlQuery.simple(query1), GsJsonStore.class);

I'm querying for a K-V pair within the _object Map. I get an error : No mapping metadata found for java.lang.Object

Why is this happening? Also, I'm storing a json object as Map<String,Object> in Couchbase, I tried using the jackson JsonNode type but the object was also storing class related metadata. Is there a better datatype to represent the json type?

EDIT

Data stored in Couchbase :

{
"_object" : {
"Name" : "Kapil",
"Age" : {
"Nested" : 21
}
},
"_subject" : "Subject",
"_predicate" : "Predicate"
}

The key I'm looking for is _object.Name and value is 'Kapil'

Kapil Earanky
  • 211
  • 4
  • 18
  • what is the type of the data that you stored inside the map? what are the type and values of the `key` and `value` in your query? – Simon Baslé Mar 15 '16 at 09:45
  • 1
    on a side note, having a long id works but it would be preferable to have a string id that is more carefully crafter (eg. with a "gsonStore" prefix and a number). All documents are stored in the same bucket by default in Couchbase, and so numerical ids greatly increase the chance of collisions and overwriting, if you are not extra careful to ALWAYS use the same source for ALL ids of ALL types of entities... – Simon Baslé Mar 15 '16 at 09:48
  • I've edited the question to show the data I'm storing in the map. Thank you for the heads up about long ids, will make the change! – Kapil Earanky Mar 15 '16 at 09:58
  • another problem is that the query you made doesn't include a filter on the `_class` field in the WHERE clause: should be `WHERE "+path+"="+"'"+value+"' AND _class = '" + GsJsonStore.class.getName() + "'"` – Simon Baslé Mar 15 '16 at 11:08
  • is the data in `_object` really always changing, or would you be able to create a class (eg. `SubObject`) that has (nullable) attributes for all possible data fields? Can you also try adding an empty private constructor to `GsJsonStore`? – Simon Baslé Mar 15 '16 at 11:08

2 Answers2

5

As explained here you should override the default SPMappingCouchbaseConverter.

Here is how I solved the problem:

@Bean
public MappingCouchbaseConverter mappingCouchbaseConverter() throws Exception {
    return new MyMappingCouchbaseConverter(couchbaseMappingContext());
}

private class MyMappingCouchbaseConverter extends MappingCouchbaseConverter {

    MyMappingCouchbaseConverter(MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext) {
        super(mappingContext);
    }

    @Override
    @SuppressWarnings("unchecked")
    protected <R> R read(final TypeInformation<R> type, final CouchbaseDocument source, final Object parent) {
        if (Object.class == typeMapper.readType(source, type).getType()) {
            return (R) source.export();
        } else {
            return super.read(type, source, parent);
        }
    }

}
rashtao
  • 657
  • 5
  • 16
  • I am stuck on that same error and when I paste this code an error starting coming on the return statement inside the @Bean. What should I pass in the couchbaseMappingContext() – Nikhil Nov 14 '18 at 11:44
  • 2
    This approach will fail if you have a payload like this inside a map. "myCustomMap":{"array":[{}]} – Nikhil Jan 14 '19 at 06:03
0

If you absolutely need the Map:

Looks like the problem is twofold:

  1. re-instantiation by reflection using a constructor with parameters doesn't seem to work that well from my tests, so I'd add a private empty constructor private GsJsonStore() {}.
  2. The Map you store contains another level of nesting, as a generic Map as well. This is problematic for the framework to deal with at deserialization time.

You could try to either flatten "Age" into the top-level _object map or create a dedicated class for the "Age" entry (simply with a Nested field of type int) and store that instead. Note that in the later case, the framework should add _class metadata to the "Age" JSON that is stored, which explains how in this case it manages to deserialize it later on.

If you can modelize more specifically than a Map:

The best way would still be to create a proper GsEntity class without generic collections/maps but named attributes (and possibly meaningful entity classes for sub values as well).

This would mitigate the problems with Maps and allow for automatic creation of the query by simply adding a method signature in the repository interface. Something like that:

public class ContentObject {

    private String name;
    private int age;

    public ContentObject(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

@Document(expiry = 0)
public class GsEntity implements Serializable {

    private static final long serialVersionUID = 7133072282172062535L;

    @Id
    private String id;

    @Field
    private ContentObject object;

    @Field
    private String subject;

    @Field
    private String predicate;

    //EMPTY CONSTRUCTOR

    //Getters and Setters 
}

public interface GsEntityRepository extends CouchbaseRepository<GsEntity, String> {

    @Query
    List<GsEntity> findByObjectNameEquals(String nameValue);
}

Note that the attributes have been renamed to remove the underscore, as it doesn't follow Java naming conventions and it would prevent the interface's method to be properly mapped to the attribute.

The method is analyzed roughly like this: findBy Object Name Equals. This translate to a SELECT of GsEntity objects which object field has a name field which has the same value as the string passed as a parameter.

Simon Baslé
  • 27,105
  • 5
  • 69
  • 70
  • The _object field in the sample json I provided could be any json object, it's schema-less hence my need to use a Map/JsonObject/JsonNode(Jackson) to store it. – Kapil Earanky Mar 16 '16 at 03:20