Search code examples
androidkotlinandroid-roomandroid-adapterandroid-room-relation

How to accessing data using Room DAOs to retrieve data from multiple tables


I'm trying hard but I get no result, I want to get all the information about the product besides the name of the category, and the name of the unit because I want to show all of this information inside my adapter what is the best practice to do it, please check my codes.

How to write a query to access data from multiple tables. And which type of class do I need to define to cover all of this data together.

@Entity(tableName = "product")
data class Product(
    @PrimaryKey(autoGenerate = true)
    val product_id: Int,

    val name: String,
    val sellPrice: Double,
    val barcode: String?,
    val buyPrice: Double?,
    val quantity: Double?,
    val packing: Double?,
    val reducePrice: Double?,
    val description: String?,

    val fk_category: Int?,
    val fk_unit: Int?
)

@Entity(tableName = "category")
data class Category(
    @PrimaryKey(autoGenerate = true)
    val category_id: Int,

    val name: String
)

@Entity(tableName = "unit")
data class Unit(
    @PrimaryKey(autoGenerate = true)
    val unit_id: Int,

    val name: String
)

data class ProductAndCategory(
    @Embedded
    val category: Category,
    @Relation(
        parentColumn = "category_id",
        entityColumn = "fk_category"
    )
    val product: Product
)

data class ProductAndUnit(
    @Embedded
    val unit: Unit,
    @Relation(
        parentColumn = "unit_id",
        entityColumn = "fk_unit"
    )
    val product: Product
)

@Dao
interface ProductDao {

    @Query("SELECT * FROM product")
    fun getAllProducts(): LiveData<List<Product>>

    @Transaction
    @Query("SELECT * FROM product INNER JOIN category on product.fk_category = category.category_id")
    fun getAllProductAndCategory(): LiveData<List<ProductAndCategory>>

    @Query("SELECT * FROM product WHERE product_id = :productID")
    suspend fun getProduct(productID: Int): List<Product>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertProduct(product: Product)

    @Update
    suspend fun updateProduct(product: Product)

    @Delete
    suspend fun deleteProduct(product: Product)

    @Query("DELETE FROM product")
    suspend fun deleteAllProducts()

    @Transaction
    @Query("SELECT * FROM product INNER JOIN category on product.fk_category = category.category_id WHERE product_id= :productID")
    suspend fun getProductAndCategory(productID: Int): List<ProductAndCategory>

    @Transaction
    @Query("SELECT * FROM product INNER JOIN unit on product.fk_unit = unit.unit_id WHERE product_id= :productID")
    suspend fun getProductAndUnit(productID: Int): List<ProductAndUnit>
}

Solution

  • You can do this two ways.

    You can Embed product and have an @Relation for Category and for Unit (2 @Realtion) or you can Embed all three.

    So you could have :-

    data class ProductAndCategoryAndUnit(
        @Embedded
        val product: Product,
        @Relation(entity = Category::class, parentColumn = "fk_category",entityColumn = "category_id")
        val category: Category,
        @Relation(entity = Unit::class, parentColumn = "fk_unit",entityColumn = "unit_id")
        val unit: Unit
    )
    

    with something like :-

    @Query("SELECT * FROM product")
    @Transaction
    fun getProductAndCategoryAndUnit(): List<ProductAndCategoryAndUnit>
    

    or alternately (BUT READ THE NOTES) :-

    data class ProductCategoryUnit (
        @Embedded
        val product: Product,
        @Embedded
        val category: Category,
        @Embedded
        val unit: Unit
    )
    

    with something like :-

    @Query("SELECT * FROM product JOIN category ON product.fk_category = category.category_id JOIN unit ON product.fk_unit = unit.unit_id")
        fun getProductCategoryUnit(): List<ProductCategoryUnit>
    

    Notes re the 2nd

    This alternative would have an issue with the 3 columns all named name (they would be ambiguous). You could disambiguate using the @Embedded(prefix = "?") however this then requires the columns to be aliased in the SQL. A simpler way would be to ensure that the column names will not be ambiguous. This can easily be achieved using the @ColumnInfo's name argument as an example for Unit (similar would be required for Category) :-

    @Entity(tableName = "unit")
    data class Unit(
        @PrimaryKey(autoGenerate = true)
        val unit_id: Long,
        @ColumnInfo(name = "unit_name") // ADDED to disambiguate for joins
        val name: String
    )
    
    • note really id's should be Long not Int.

    Working Example of Both

    Based upon your code the following demonstrates both, noting that for convenience and brevity the demo is run on the main thread so LiveData and suspended functions have been altered accordingly. Long's have also been used instead of Int's for id's.

    The Product data class used :-

    @Entity(tableName = "product")
    data class Product(
        @PrimaryKey(autoGenerate = true)
        val product_id: Long,
    
        val name: String,
        val sellPrice: Double,
        val barcode: String?,
        val buyPrice: Double?,
        val quantity: Double?,
        val packing: Double?,
        val reducePrice: Double?,
        val description: String?,
        val fk_category: Long?,
        val fk_unit: Long?
    )
    

    The Category data class used :-

    @Entity(tableName = "category")
    data class Category(
        @PrimaryKey(autoGenerate = true)
        val category_id: Long,
        @ColumnInfo(name = "category_name") // ADDED to disambiguate for joins
        val name: String
    )
    

    The data class Unit is as above as are the POJO's.

    The ProductDao used for the demo :-

    @Dao
    interface ProductDao {
    
        @Query("SELECT * FROM product")
        fun getAllProducts(): /*LiveData<*/List<Product>/*>*/
    
        /*
        @Transaction
        @Query("SELECT * FROM product INNER JOIN category on product.fk_category = category.category_id")
        fun getAllProductAndCategory(): LiveData<List<ProductAndCategory>>
        */
    
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(unit: Unit): Long
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(category: Category): Long
    
    
        @Query("SELECT * FROM product WHERE product_id = :productID")
        suspend fun getProduct(productID: Int): List<Product>
    
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        /*suspend*/ fun insertProduct(product: Product)
    
        @Update
        suspend fun updateProduct(product: Product)
    
        @Delete
        suspend fun deleteProduct(product: Product)
    
        @Query("DELETE FROM product")
        suspend fun deleteAllProducts()
    
        /*
        @Transaction
        @Query("SELECT * FROM product INNER JOIN category on product.fk_category = category.category_id WHERE product_id= :productID")
        suspend fun getProductAndCategory(productID: Int): List<ProductAndCategory>
        */
    
        /*
        @Transaction
        @Query("SELECT * FROM product INNER JOIN unit on product.fk_unit = unit.unit_id WHERE product_id= :productID")
        suspend fun getProductAndUnit(productID: Int): List<ProductAndUnit>
        */
    
        @Query("SELECT * FROM product")
        @Transaction
        fun getProductAndCategoryAndUnit(): List<ProductAndCategoryAndUnit>
    
        @Query("SELECT * FROM product WHERE product_id=:productID")
        fun getProductAndCategoryAndUnitById(productID: Long): List<ProductAndCategoryAndUnit>
    
        @Query("SELECT * FROM product JOIN category ON product.fk_category = category.category_id JOIN unit ON product.fk_unit = unit.unit_id")
        fun getProductCategoryUnit(): List<ProductCategoryUnit>
    
    }
    

    The TheDatabase class is a pretty stock @Database so is not included.

    Lastly is an Activity as per :-

    class MainActivity : AppCompatActivity() {
        lateinit var db: TheDatabase
        lateinit var dao: ProductDao
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            db = TheDatabase.getInstance(this)
            dao = db.getProductDao()
    
            var u1 = dao.insert(Unit(0L, "Unit1"))
            var u2 = dao.insert(Unit(0L, "Unit2"))
    
            var c1 = dao.insert(Category(0L,"Cat1"))
            var c2 = dao.insert(Category(0L,"Cat2"))
    
            dao.insertProduct(Product(0L,"Produuct1",1.1,"Barcode001",2.2,10.5,0.0,0.0,"Desc4Product1",c2,u1))
            dao.insertProduct(Product(0L,"Product2",1.1,"Barcode002",2.2,10.5,0.0,0.0,"Desc4product2",c1,u2))
    
            // Inserts Catergory,Unit and Product (utilising the id returned by the inserts for the category and Unit)
            dao.insertProduct(
                Product(
                    0L,"" +
                            "Product3",
                    1.1,"" +
                            "Barcode003",
                    2.2,
                    10.5,
                    0.0,
                    0.0,
                    "Desc4Product3",
                    dao.insert(Category(0L,"Cat4")),
                    dao.insert(Unit(0L,"Unit4"))
                )
            )
    
            // Extract and Log via the @Embedded and 2 @Realtion's
            for(pcu: ProductAndCategoryAndUnit in dao.getProductAndCategoryAndUnit()) {
                Log.d("PRODUCTINFO","Product is ${pcu.product.name} .... Category is ${pcu.category.name} Unit is ${pcu.unit.name}")
            }
            // Extract and Log via the 3 @Embedded's
            for(pcu: ProductCategoryUnit in dao.getProductCategoryUnit()) {
                Log.d("PRODUCTINFO","Product is ${pcu.product.name} .... Category is ${pcu.category.name} Unit is ${pcu.unit.name}")
            }
        }
    }
    

    The result in the Log :-

    D/PRODUCTINFO: Product is Produuct1 .... Category is Cat2 Unit is Unit1
    D/PRODUCTINFO: Product is Product2 .... Category is Cat1 Unit is Unit2
    D/PRODUCTINFO: Product is Product3 .... Category is Cat4 Unit is Unit4
    
    
    D/PRODUCTINFO: Product is Produuct1 .... Category is Cat2 Unit is Unit1
    D/PRODUCTINFO: Product is Product2 .... Category is Cat1 Unit is Unit2
    D/PRODUCTINFO: Product is Product3 .... Category is Cat4 Unit is Unit4
    

    i.e. the results of both methods are identical.