Skip to content

[2] Entity MapWrapper

cyrillhalter edited this page Jul 30, 2024 · 4 revisions
  1. Add it in your root build.gradle at the end of repositories:
 buildscript {
    repositories {
        maven { url 'https://jitpack.io' }
    }
  }
  ...
  allprojects {
    repositories {
	maven { url 'https://jitpack.io' }
    }
  }
  1. Add gradle dependency
  implementation 'com.github.Kaufland.andcouchbaseentity:couchbase-entity-api:3.1.0'
  kapt 'com.github.Kaufland.andcouchbaseentity:couchbase-entity:3.1.0'
  1. Optionally, if you use Couchbase 2.x.x (provides already implemented connector)
  implementation 'com.github.Kaufland.andcouchbaseentity:couchbase-entity-connector:3.1.0@aar'

NOTE: For custom connectors see

Connector

Example

  1. Configure library
  • Add the following code in your Application.kt
  @Override
  public void onCreate() {
      super.onCreate();
           PersistenceConfig.configure(object : Couchbase2Connector() {
            override fun getDatabase(name: String): Database {
                if (DB == name) {
                    //for better performance when using queries may consider create some Indexes
                    return database!!
                }
                throw RuntimeException("wrong db name defined!!")
            }
        })
  }

NOTE: For other databases implement your own connector

  • Annotate classes to generate entities (all generated classes have the suffix Entity or Wrapper (used for child entities or map wrapping)
@Entity(database = Application.DB)
@Fields(
        Field(name = "type", type = String::class, defaultValue = "product", readonly = true),
        Field(name = "name", type = String::class),
        Field(name = "comments", type = UserComment::class, list = true),
        Field(name = "image", type = Blob::class),
        Field(name = "identifiers", type = String::class, list = true)
)
@Queries(
        Query(fields = ["type"])
)
open class Product{

    companion object{

        @GenerateAccessor
        fun someComplexQuery(param1 : String){
            //do some heavy logic here
        }
    }
}
  • Use generated classes and be happy :-)

There are two different ways to interact with the entity / mapWrapper

  • Example 1: use fluent API to modify multiple properties.
ProductEntity.create().builder()
                       .setName("Wodka")
                         .setComments(listOf(UserCommentWrapper
                           .create().builder()
                             .setComment("feeling like touch the sky")
                               .exit())).setImage(Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder)))
                                  .exit().save()
  • Example 2: get / set data via kotlin property syntax
var data = ProductEntity.create()
    data.name = "Tomatoes"
    data.comments = listOf(UserCommentWrapper.create().builder().setComment("don't like there color").exit(),
               UserCommentWrapper.create().builder().setComment("worst experience ever!!").exit())
    data.image = Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder))
    data.save()

NOTE : To modify child entities it's neccessary to invoke the setter before saving the parent entity

 final List<UserCommentWrapper> data = getParentEntity().getComments();
 data.remove(position);
 try {
     ProductEntity entity = getParentEntity();
     entity.setComments(mComments);
     entity.save();
     } catch (PersistenceException e) {
     Log.e(TAG, "failed to save Entity", e);
     }

Queries

By using the @Query annotation the Framework automatically generates static methods to execute this queries and returns the resulted entities

@Fields(
       Field(name = "type", type = String::class, defaultValue = "product", readonly = true),
       Field(name = "name", type = String::class)
)
@Queries(
       Query(fields = ["type"]),
       Query(fields = ["type", "name"])
)

Above Code generates Methods which can be used like

val resultByType = MyEntity.findByType()
val resultByTypeAndName = MyEntity.findbyTypeAndName("beer")

The Query generation may not fit all needs for this cases you can define a query by yourself and use the @GenerateAccessor annotation to make it available in the GeneratedEntity

@Entity(database = "mydb_db")
//Fields and Queries add them here
open class Product{

   companion object{

       @GenerateAccessor
       fun someComplexQuery(param1 : String){
           //do some heavy logic here
       }
   }
}
ProductEntity.someComplexQuery("Foo")

Schemas

By using the @SchemaClass annotation the Framework automatically generates a class that contains all the fields as properties. The fields are mapped to one of four types, determined by the parameters provided for each field, along with a generic derived from the "type" parameter.

The type is determined based on the following criteria:

  1. CMList: When the "list" parameter is marked as true.
  2. CMObject: If the "type" parameter is assigned to another class that also has the @SchemaClass annotation.
  3. CMObjectList: When both the "list" parameter is true and the "type" parameter is set to a class that is annotated with @SchemaClass.
  4. CMField: When none of the above apply.

If there is a defaultValue given to a Field, it also creates a default property for this field.

@SchemaClass
@Fields(
        Field(name = "type", type = String::class, defaultValue = "product", readonly = true),
        Field(name = "name", type = String::class)
)
class Product

The Code above generates a class like this:

open class ProductSchema(
    path: String = ""
) : Schema {
    val DEFAULT_TYPE: String = "product"

    val type: CMField<String> = CMField("type", path)

    val name: CMField<String> = CMField("name", path)
}

This class can then be used like this:

val productSchema = ProductSchema()
val productNameField = productSchema.name

TypeConverters (since Crystal-Map 4.0.0)

Crystal-Map stores data in maps as one of the following types:

String
Boolean
Number
Map
Any

Any other type that is used in a Field annotation requires a TypeConverter to be defined. Crystal-Map ensures this at annotation processing time.

A TypeConverter can be defined using the annotation @TypeConverter. This annotation should be placed on an abstract or open class that implements the ITypeConverter interface. The generic type parameters of the interface specify the type we want to convert to (i.e. the one that is used in the Field annotation) and the type that should be used in the map (i.e. one of the types described above). The following is an example that converts between LocalDate and String.

@TypeConverter
abstract class LocalDateConverter : ITypeConverter<LocalDate, String> {
    override fun write(value: LocalDate?): String? =
        value?.toString()

    override fun read(value: String?): LocalDate? = value?.let { LocalDate.parse(it) }
}

This will generate a Kotlin object that will be used in the generated Entities and Wrappers

public object LongConverterInstance : LongConverter()

TypeConverter Import/Export

In order to facilitate the usage of TypeConverters across modules and projects, they can be exported in the source module and imported in the target module.

A TypeConverter export can be defined with the @TypeConverterExporter annotation. The annotation should be placed on an interface.

@TypeConverterExporter
interface DummyTypeConverters

This will generate a Kotlin class that includes all specified TypeConverters in a format that can be used by the annotation processor:

public class DummyTypeConvertersInstance : DummyTypeConverters, ITypeConverterExporter {
  override val typeConverters: Map<KClass<*>, ITypeConverter<*, *>>
    get() = mapOf(
      Date::class to DummyDateConverterInstance,
    )

  override val typeConverterImportables: List<TypeConverterImportable>
    get() = listOf(
      TypeConverterImportable(
        ClassNameDefinition("com.schwarz.crystaldummy.customtypes", "DummyDateConverterInstance"), 
        ClassNameDefinition("java.util", "Date"), 
        ClassNameDefinition("kotlin", "Number")
      ),
    )
}

The @TypeConverterImporter can then be used to import the exported TypeConverters. For this, the aforementioned class is passed as a parameter to the annotation.

@TypeConverterImporter(DummyTypeConvertersInstance::class)
interface DummyTypeConverterImporter

The exported TypeConverters will then be added to the list of available converters and used by the annotation processor.