Extract database module

This commit is contained in:
Kirill Kamakin
2022-08-04 19:34:21 +02:00
parent 7ff575e72d
commit 53b40bbc60
49 changed files with 159 additions and 112 deletions

View File

@@ -0,0 +1 @@
<manifest />

View File

@@ -0,0 +1,30 @@
package gq.kirmanak.mealient.database
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import gq.kirmanak.mealient.database.recipe.RecipeDao
import gq.kirmanak.mealient.database.recipe.entity.*
@Database(
version = 2,
entities = [
CategoryEntity::class,
CategoryRecipeEntity::class,
TagEntity::class,
TagRecipeEntity::class,
RecipeSummaryEntity::class,
RecipeEntity::class,
RecipeIngredientEntity::class,
RecipeInstructionEntity::class
],
exportSchema = true,
autoMigrations = [
AutoMigration(from = 1, to = 2)
]
)
@TypeConverters(RoomTypeConverters::class)
abstract class AppDb : RoomDatabase() {
abstract fun recipeDao(): RecipeDao
}

View File

@@ -0,0 +1,22 @@
package gq.kirmanak.mealient.database
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
interface DatabaseModule {
companion object {
@Provides
@Singleton
fun createDb(@ApplicationContext context: Context): AppDb =
Room.databaseBuilder(context, AppDb::class.java, "app.db").build()
}
}

View File

@@ -0,0 +1,22 @@
package gq.kirmanak.mealient.database
import androidx.room.TypeConverter
import kotlinx.datetime.*
object RoomTypeConverters {
@TypeConverter
fun localDateTimeToTimestamp(localDateTime: LocalDateTime) =
localDateTime.toInstant(TimeZone.UTC).toEpochMilliseconds()
@TypeConverter
fun timestampToLocalDateTime(timestamp: Long) =
Instant.fromEpochMilliseconds(timestamp).toLocalDateTime(TimeZone.UTC)
@TypeConverter
fun localDateToTimeStamp(date: LocalDate) =
localDateTimeToTimestamp(date.atTime(0, 0))
@TypeConverter
fun timestampToLocalDate(timestamp: Long) =
timestampToLocalDateTime(timestamp).date
}

View File

@@ -0,0 +1,76 @@
package gq.kirmanak.mealient.database.recipe
import androidx.paging.PagingSource
import androidx.room.*
import gq.kirmanak.mealient.database.recipe.entity.*
@Dao
interface RecipeDao {
@Query("SELECT * FROM tags")
suspend fun queryAllTags(): List<TagEntity>
@Query("SELECT * FROM categories")
suspend fun queryAllCategories(): List<CategoryEntity>
@Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC")
fun queryRecipesByPages(): PagingSource<Int, RecipeSummaryEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipe(recipeSummaryEntity: RecipeSummaryEntity)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertTag(tagEntity: TagEntity): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertTagRecipeEntity(tagRecipeEntity: TagRecipeEntity)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertCategory(categoryEntity: CategoryEntity): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertCategoryRecipeEntity(categoryRecipeEntity: CategoryRecipeEntity)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertTagRecipeEntities(tagRecipeEntities: Set<TagRecipeEntity>)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertCategoryRecipeEntities(categoryRecipeEntities: Set<CategoryRecipeEntity>)
@Query("DELETE FROM recipe_summaries")
suspend fun removeAllRecipes()
@Query("DELETE FROM tags")
suspend fun removeAllTags()
@Query("DELETE FROM categories")
suspend fun removeAllCategories()
@Query("SELECT * FROM recipe_summaries ORDER BY date_updated DESC")
suspend fun queryAllRecipes(): List<RecipeSummaryEntity>
@Query("SELECT * FROM category_recipe")
suspend fun queryAllCategoryRecipes(): List<CategoryRecipeEntity>
@Query("SELECT * FROM tag_recipe")
suspend fun queryAllTagRecipes(): List<TagRecipeEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipe(recipe: RecipeEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipeInstructions(instructions: List<RecipeInstructionEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipeIngredients(ingredients: List<RecipeIngredientEntity>)
@Transaction
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) // The lint is wrong, the columns are actually used
@Query("SELECT * FROM recipe JOIN recipe_summaries ON recipe.remote_id = recipe_summaries.remote_id JOIN recipe_ingredient ON recipe_ingredient.recipe_id = recipe.remote_id JOIN recipe_instruction ON recipe_instruction.recipe_id = recipe.remote_id WHERE recipe.remote_id = :recipeId")
suspend fun queryFullRecipeInfo(recipeId: Long): FullRecipeInfo?
@Query("DELETE FROM recipe_ingredient WHERE recipe_id = :recipeId")
suspend fun deleteRecipeIngredients(recipeId: Long)
@Query("DELETE FROM recipe_instruction WHERE recipe_id = :recipeId")
suspend fun deleteRecipeInstructions(recipeId: Long)
}

View File

@@ -0,0 +1,12 @@
package gq.kirmanak.mealient.database.recipe.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "categories", indices = [Index(value = ["name"], unique = true)])
data class CategoryEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
@ColumnInfo(name = "name") val name: String,
)

View File

@@ -0,0 +1,29 @@
package gq.kirmanak.mealient.database.recipe.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
@Entity(
tableName = "category_recipe",
primaryKeys = ["category_id", "recipe_id"],
indices = [Index(value = ["category_id", "recipe_id"], unique = true)],
foreignKeys = [ForeignKey(
entity = CategoryEntity::class,
parentColumns = ["local_id"],
childColumns = ["category_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
), ForeignKey(
entity = RecipeSummaryEntity::class,
parentColumns = ["remote_id"],
childColumns = ["recipe_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)]
)
data class CategoryRecipeEntity(
@ColumnInfo(name = "category_id") val categoryId: Long,
@ColumnInfo(name = "recipe_id", index = true) val recipeId: Long
)

View File

@@ -0,0 +1,23 @@
package gq.kirmanak.mealient.database.recipe.entity
import androidx.room.Embedded
import androidx.room.Relation
data class FullRecipeInfo(
@Embedded val recipeEntity: RecipeEntity,
@Relation(
parentColumn = "remote_id",
entityColumn = "remote_id"
)
val recipeSummaryEntity: RecipeSummaryEntity,
@Relation(
parentColumn = "remote_id",
entityColumn = "recipe_id"
)
val recipeIngredients: List<RecipeIngredientEntity>,
@Relation(
parentColumn = "remote_id",
entityColumn = "recipe_id"
)
val recipeInstructions: List<RecipeInstructionEntity>,
)

View File

@@ -0,0 +1,11 @@
package gq.kirmanak.mealient.database.recipe.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "recipe")
data class RecipeEntity(
@PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: Long,
@ColumnInfo(name = "recipe_yield") val recipeYield: String,
)

View File

@@ -0,0 +1,17 @@
package gq.kirmanak.mealient.database.recipe.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "recipe_ingredient")
data class RecipeIngredientEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
@ColumnInfo(name = "recipe_id") val recipeId: Long,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "note") val note: String,
@ColumnInfo(name = "unit") val unit: String,
@ColumnInfo(name = "food") val food: String,
@ColumnInfo(name = "disable_amount") val disableAmount: Boolean,
@ColumnInfo(name = "quantity") val quantity: Int,
)

View File

@@ -0,0 +1,13 @@
package gq.kirmanak.mealient.database.recipe.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "recipe_instruction")
data class RecipeInstructionEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
@ColumnInfo(name = "recipe_id") val recipeId: Long,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "text") val text: String,
)

View File

@@ -0,0 +1,23 @@
package gq.kirmanak.mealient.database.recipe.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
@Entity(tableName = "recipe_summaries")
data class RecipeSummaryEntity(
@PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: Long,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "slug") val slug: String,
@ColumnInfo(name = "image") val image: String?,
@ColumnInfo(name = "description") val description: String,
@ColumnInfo(name = "rating") val rating: Int?,
@ColumnInfo(name = "date_added") val dateAdded: LocalDate,
@ColumnInfo(name = "date_updated") val dateUpdated: LocalDateTime
) {
override fun toString(): String {
return "RecipeSummaryEntity(remoteId=$remoteId, name='$name')"
}
}

View File

@@ -0,0 +1,12 @@
package gq.kirmanak.mealient.database.recipe.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "tags", indices = [Index(value = ["name"], unique = true)])
data class TagEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
@ColumnInfo(name = "name") val name: String
)

View File

@@ -0,0 +1,27 @@
package gq.kirmanak.mealient.database.recipe.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(
tableName = "tag_recipe",
primaryKeys = ["tag_id", "recipe_id"],
foreignKeys = [ForeignKey(
entity = TagEntity::class,
parentColumns = ["local_id"],
childColumns = ["tag_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
), ForeignKey(
entity = RecipeSummaryEntity::class,
parentColumns = ["remote_id"],
childColumns = ["recipe_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)]
)
data class TagRecipeEntity(
@ColumnInfo(name = "tag_id") val tagId: Long,
@ColumnInfo(name = "recipe_id", index = true) val recipeId: Long
)

View File

@@ -0,0 +1,36 @@
package gq.kirmanak.mealient.database
import com.google.common.truth.Truth.assertThat
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import org.junit.Test
class RoomTypeConvertersTest {
@Test
fun `when localDateTimeToTimestamp then correctly converts`() {
val input = LocalDateTime.parse("2021-11-13T15:56:33")
val actual = RoomTypeConverters.localDateTimeToTimestamp(input)
assertThat(actual).isEqualTo(1636818993000)
}
@Test
fun `when timestampToLocalDateTime then correctly converts`() {
val expected = LocalDateTime.parse("2021-11-13T15:58:38")
val actual = RoomTypeConverters.timestampToLocalDateTime(1636819118000)
assertThat(actual).isEqualTo(expected)
}
@Test
fun `when localDateToTimeStamp then correctly converts`() {
val input = LocalDate.parse("2021-11-13")
val actual = RoomTypeConverters.localDateToTimeStamp(input)
assertThat(actual).isEqualTo(1636761600000)
}
@Test
fun `when timestampToLocalDate then correctly converts`() {
val expected = LocalDate.parse("2021-11-13")
val actual = RoomTypeConverters.timestampToLocalDate(1636761600000)
assertThat(actual).isEqualTo(expected)
}
}