Skip to content

Data model

Albert Pinto edited this page Jul 13, 2021 · 1 revision

In the application, there are two sources of data: local and remote. The local database is used to store the recipes of the current user, whilst the remote one is used to get more recipes from an API. In this document, we are going to describe how to access the data inside the application.

Local Data

The local database is used to store the user's recipes and favourite ones. ***MySQL *** manages it, and the application uses Room library to perform operations on it.

Entities

The classes represented in the application have an Entity, representing a table in the database. By convention, all properties from Kotlin classes have to be written in camel case; however, attributes and table names from databases should be written in lowercase with underscores.

Each entity is a data class annotated with @Entity and has a name passed as an argument to the annotation.

@Entity(tableName = "recipes")
data class Recipe(
    // ...
)

Next we need to define the attributes of the entity. In order to specify the name of the attribute in the table we have to annotate the Kotlin properties with @ColumnInfo and passing the name as a parameter. The name of the attribute will be the property written in lowercase and with underscores.

@Entity(tableName = "recipes")
data class Recipe(
    @ColumnInfo(name = "recipe_id")
    val recipeId: Long,

    @ColumnInfo(name = "name")
    val name: String,

    @ColumnInfo(name = "type")
    val type: RecipeType,

    @ColumnInfo(name = "description")
    val description: String,

    @ColumnInfo(name = "time")
    val time: Int,

    @ColumnInfo(name = "steps")
    val steps: List<String>
)

After defining the properties of the class, we need to specify the primary key of the entity, which is the property that identifies every instance. It is annotated with @PrimaryKey and might be autogenerated. In this case, it must always be a Long, and when creating the instance, the value 0L should be passed.

@Entity(tableName = "recipes")
data class Recipe(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "recipe_id")
    val recipeId: Long,
    // ...
)

Note that there are some type converters defined in Converters.kt, which are used to deal with enums and collections in database.

@TypeConverter
fun toRecipeType(value: String) = enumValueOf<RecipeType>(value)

@TypeConverter
fun fromRecipeType(value: RecipeType) = value.name

Relations

In addition, some relations have been implemented as classes due to Room requirements. These are located in the relations package and might get information from the database since there are entities. Suppose we want to create a relation between Recipe and Ingredient since every recipe has at least one ingredient, and every ingredient can belong to some recipes. On the one hand, we define the cross-reference, which will be called RecipeIngredientCrossRef. The name order does not matter, but it is strongly recommended to put first the entity from which we will access the relation more frequently. The class has to be annotated with @Entity and has a property for each primary key in the relation. The primary key of these entities will be the composition of both properties. In addition, you might add as many additional attributes as you wish.

@Entity(tableName = "recipe_ingredient", primaryKeys = ["recipe_id", "ingredient_name"])
data class RecipeIngredientCrossRef(
    @ColumnInfo(name = "recipe_id")
    val recipeId: Long,

    @ColumnInfo(name = "ingredient_name")
    val ingredientName: String,

    @ColumnInfo(name = "quantity")
    val quantity: Double
)

On the other hand, we need to define the relation itself. The class will be called RecipeWithIngredient since we want to get all ingredients from it. The Recipe instance has to be annotated with @Embedded since it is the starting point of the relation. Since we want to know the ingredients of a recipe, we have to add a list of IngredientEntity.

data class RecipeWithIngredient(
    @Embedded val recipeEntity: RecipeEntity,
    @Relation(
        parentColumn = "recipe_id",
        entityColumn = "ingredient_name",
        associateBy = Junction(RecipeIngredientCrossRef::class)
    )
    val ingredientEntities: List<IngredientEntity>,
)

In order to improve readability, there has been defined a set of typealises to use the cross references.

typealias UserRecipe = UserRecipeCrossRef
typealias RecipeIngredient = RecipeIngredientCrossRef
typealias UserFavoriteRemoteRecipe = UserFavoriteRemoteRecipeCrossRef

Remote data

To get the remote recipes, we are using the Fuel library. We have created a DAO to use the Edamam API so that if we decide to change the API in the future, we will only have to create a class that implements the RemoteRecipeDao interface.

Clone this wiki locally