How to use spring data with couchbase without _class attribute
Asked Answered
E

5

7

Is there a simple way to use spring data couchbase with documents that do not have _class attribute? In the couchbase I have something like this in my sampledata bucket:

{
  "username" : "alice", 
  "created" : 1473292800000,
  "data" : { "a": 1, "b" : "2"},
  "type" : "mydata"
}

Now, is there any way to define mapping from this structure of document to Java object (note that _class attribute is missing and cannot be added) and vice versa so that I get all (or most) automagical features from spring couchbase data?

Something like: If type field has value "mydata" use class MyData.java. So when find is performed instead of automatically adding AND _class = "mydata" to generated query add AND type = "mydata".

Edmondedmonda answered 9/8, 2016 at 9:57 Comment(0)
S
7

Spring Data in general needs the _class field to know what to instantiate back when deserializing.

It's fairly easy in Spring Data Couchbase to use a different field name than _class, by overriding the typeKey() method in the AbsctractCouchbaseDataConfiguration.

But it'll still expect a fully qualified classname in there by default

Getting around that will require quite a bit more work:

  1. You'll need to implement your own CouchbaseTypeMapper, following the model of DefaultCouchbaseTypeMapper. In the super(...) constructor, you'll need to provide an additional argument: a list of TypeInformationMapper. The default implementation doesn't explicitly provide one, so a SimpleTypeInformationMapper is used, which is the one that puts FQNs.
  2. There's an alternative implementation that is configurable so you can alias specific classes to a shorter name via a Map: ConfigurableTypeInformationMapper...
  3. So by putting a ConfigurableTypeInformationMapper with the alias you want for specific classes + a SimpleTypeInformationMapper after it in the list (for the case were you serialize a class that you didn't provide an alias for), you can achieve your goal.
  4. The typeMapper is used within the MappingCouchbaseConverter, which you'll also need to extend unfortunately (just to instantiate your typeMapper instead of the default.
  5. Once you have that, again override the configuration to return an instance of your custom MappingCouchbaseConverter that uses your custom CouchbaseTypeMapper (the mappingCouchbaseConverter() method).
Son answered 19/8, 2016 at 11:4 Comment(4)
Simon, When you said that the FQN is expected by default. Does it mean that it can be overriden? I'm asking this, because I tried what you have recommanded, it worked while persisting in the database, but while retrieving (eg. findByField(...)), the typeKey filter is using the FQN of the target class, ie ignoring the custom converter.Epirogeny
Can you confirm that it is not possible to use an alias instead of the fully qualified classname when using Spring's query methods? I draw this conclusion looking N1qlUtils#createWhereFilterForEntityLeveille
Correct, AFAIK right now this is not possible, or at least not officially supportedFlowing
@SimonBaslé perhaps the answer can be amended to specify the limitations. Of this approach and that it will end up not working.Tracey
S
5

You can achive this e.g. by creating custom annotation @DocumentType

@DocumentType("billing")
@Document
public class BillingRecordDocument {
    String name;
    // ...
}

Document will look like:

{
    "type" : "billing"
    "name" : "..."
}

Just create following classes: Create custom AbstractReactiveCouchbaseConfiguration or AbstractCouchbaseConfiguration (depends which varian you use)

@Configuration
@EnableReactiveCouchbaseRepositories
public class CustomReactiveCouchbaseConfiguration extends AbstractReactiveCouchbaseConfiguration {
     // implement abstract methods
     // and configure custom mapping convereter
    @Bean(name = BeanNames.COUCHBASE_MAPPING_CONVERTER)
    public MappingCouchbaseConverter mappingCouchbaseConverter() throws Exception {
        MappingCouchbaseConverter converter = new CustomMappingCouchbaseConverter(couchbaseMappingContext(), typeKey());
        converter.setCustomConversions(customConversions());
        return converter;
    }

    @Override
    public String typeKey() {
        return "type"; // this will owerride '_class'
    }
}

Create custom MappingCouchbaseConverter

public class CustomMappingCouchbaseConverter extends MappingCouchbaseConverter {

    public CustomMappingCouchbaseConverter(final MappingContext<? extends CouchbasePersistentEntity<?>,
            CouchbasePersistentProperty> mappingContext, final String typeKey) {
        super(mappingContext, typeKey);
        this.typeMapper = new TypeBasedCouchbaseTypeMapper(typeKey);
    }
}

and custom annotation @DocumentType

@Persistent
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface DocumentType {

    String value();

}

Then create TypeAwareTypeInformationMapper which will just check if an entity is annoatated by @DocumentType if so, use value from that annotation, do the default if not (fully qualified class name)

public class TypeAwareTypeInformationMapper extends SimpleTypeInformationMapper {

    @Override
    public Alias createAliasFor(TypeInformation<?> type) {
        DocumentType[] documentType = type.getType().getAnnotationsByType(DocumentType.class);

        if (documentType.length == 1) {
            return Alias.of(documentType[0].value());
        }

        return super.createAliasFor(type);
    }
}

Then register it as following

public class TypeBasedCouchbaseTypeMapper extends DefaultTypeMapper<CouchbaseDocument> implements CouchbaseTypeMapper {

    private final String typeKey;

    public TypeBasedCouchbaseTypeMapper(final String typeKey) {
        super(new DefaultCouchbaseTypeMapper.CouchbaseDocumentTypeAliasAccessor(typeKey),
              Collections.singletonList(new TypeAwareTypeInformationMapper()));
        this.typeKey = typeKey;
    }

    @Override
    public String getTypeKey() {
        return typeKey;
    }
}
Sympathetic answered 12/8, 2019 at 17:21 Comment(1)
this was very helpful, I'm able to create documents with a custom type key and type value now, BUT my spring-data-couchbase repository methods like findById no longer work. From what I gather from the following answers, maybe that's because the where clause is still using the fully qualified Java class name as the type value no matter what... Did you find a way around this?Geotectonic
M
3

In your couchbase configuration class you just need to have :

@Override
public String typeKey() {
    return "type";
}

Unfortunately for query derivation (n1ql) the _class or type are still using the class name.Tried spring couch 2.2.6 and it's minus point here. @Simon, are you aware that something has changed and the support to have the possibility to have custom _class/type value in next release(s)?

Megohm answered 22/8, 2017 at 6:45 Comment(1)
I'm making a fix for n1ql and findBy* queries to use the the custom type.Stearic
A
2

@SimonBasle Inside of class N1qlUtils and method createWhereFilterForEntity we have access to the CouchbaseConverter. On line:

String typeValue = entityInformation.getJavaType().getName();

Why not use the typeMapper from the converter to get the name of the entity when we want to avoid using the class name? Otherwise you have to annotate each method in your repository as follows:

@Query("#{#n1ql.selectEntity} WHERE `type`='airport' AND airportname = $1")
List<Airport> findAirportByAirportname(String airportName);

If createWhereFilterForEntity used the CouchbaseConverter we could avoid annotating with the @Query.

Alidia answered 4/3, 2019 at 17:39 Comment(0)
D
0

This can be done by doing the following:

  1. For the key: Override typeKey() in AbstractCouchbaseConfiguration or AbstractReactiveCouchbaseConfiguration, this is "_class" by default
public class CustomCouchbaseConfiguration extends AbstractCouchbaseConfiguration {

   @Override
   public String typeKey() {
      return "type";
   }
}
  1. For the value: Annotate your entity with @TypeAlias("mydata")
@Document
@TypeAlias("mydata")
public class MyDataEntity {}

This will also automatically be added when persisting your object.

Dismount answered 30/5, 2024 at 11:45 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.