Home > Back-end >  Android Room belongsTo (one to one) relationship
Android Room belongsTo (one to one) relationship

Time:10-16

Let's say I have an Article table like this:

Article {
   id;
   title;
   content;
   main_reference_id;
   secondary_reference_id;
}

And there is a Reference table somewhat like this:

Reference {
   id;
   other_reference_related_columns...;
}

Now I want to fetch the Article while also fetching main reference, secondary reference with another POJO like this:

data class ArticleFull(
    @Embedded
    val article: article,
    @Relation(parentColumn = "main_reference_id", entityColumn = "id")
    val main_reference: Reference,
    @Relation(parentColumn = "secondary_reference_id", entityColumn = "id")
    val other_reference: Reference
)

But I'm not sure what I wrote is the right usage of @Relation annotation or not.

N.B.: I'm from the Laravel/Eloquent background, So I'm more familiar with these belongsTo, hasOne, hasMany, belongsToMany, and so on relationship types.

Thanks.

CodePudding user response:

But I'm not sure what I wrote is the right usage of @Relation annotation or not.

Yes that is fine.

Here's a working example. That shows the use of the ArticleFull POJO:-

First the Entities (Tables):-

Reference :-

@Entity
data class Reference(
    @PrimaryKey
    val id: Long? = null,
    val other_data: String
)

Article :-

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = Reference::class,
            parentColumns = ["id"],
            childColumns = ["main_reference_id"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Reference::class,
            parentColumns = ["id"],
            childColumns = ["secondary_reference_id"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ]
)
data class Article(
    @PrimaryKey
    val id: Long? = null,
    val title: String,
    val content: String,
    @ColumnInfo(index = true)
    val main_reference_id: Long,
    @ColumnInfo(index = true)
    val secondary_reference_id: Long
)
  • Foreign Key constraints added, they help to enforce referential integrity, they are optional. The onDelete and onUpdate are optional within the Foreign Key.

An @Dao class (abstract class rather than interface, abstract class is more versatile) ArticleAndReferenceDao :-

@Dao
abstract class ArticleAndReferenceDao {
    @Insert
    abstract fun insert(reference: Reference): Long
    @Insert
    abstract fun insert(article: Article): Long
    @Transaction
    @Query("SELECT * FROM article")
    abstract fun getAllArticleFull(): List<ArticleFull>
    @Transaction@Query("SELECT * FROM article WHERE id=:articleId")
    abstract fun getArticleFullByArticleId(articleId: Long): List<ArticleFull>
}

An @Database class ArticleDatabase :-

@Database(entities = [Reference::class,Article::class],version = 1)
abstract class ArticleDatabase: RoomDatabase() {
    abstract fun getArticleAndReferenceDao(): ArticleAndReferenceDao

    companion object {

        @Volatile
        private var instance: ArticleDatabase? = null

        fun getArticleDatabaseInstance(context: Context): ArticleDatabase {
            if(instance == null) {
                instance = Room.databaseBuilder(
                    context,
                    ArticleDatabase::class.java,
                    "article.db"
                )
                    .allowMainThreadQueries()
                    .build()
            }
            return instance as ArticleDatabase
        }
    }
}

Finally some Activity code , noting that for convenience and brevity .allowMainThreadQueries has been used allow the code to be run on the main thread :-

class MainActivity : AppCompatActivity() {

    lateinit var articleDatabase: ArticleDatabase
    lateinit var articleAndReferenceDao: ArticleAndReferenceDao
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        articleDatabase = ArticleDatabase.getArticleDatabaseInstance(this)
        articleAndReferenceDao = articleDatabase.getArticleAndReferenceDao()

        val ref1 = articleAndReferenceDao.insert(Reference(other_data = "Reference1"))
        val ref2 = articleAndReferenceDao.insert(Reference(other_data = "Reference2"))
        val ref3 = articleAndReferenceDao.insert(Reference(other_data = "Reference3"))
        val ref4 = articleAndReferenceDao.insert(Reference(other_data = "Reference4"))

        articleAndReferenceDao.insert(Article(title = "Article1",main_reference_id = ref1,secondary_reference_id = ref2, content = "Content for Article1"))
        articleAndReferenceDao.insert(Article(title = "Article2", main_reference_id = ref3, secondary_reference_id = ref4,content = "Content for Article2"))

        // AND/OR

        articleAndReferenceDao.insert(
            Article(
            title = "Article3",
                content = "Content for Article 3",
                main_reference_id = articleAndReferenceDao.insert(Reference(other_data = "Reference5")),
                secondary_reference_id = articleAndReferenceDao.insert(Reference(other_data = "reference6"))
            )
        )

        for(d: ArticleFull in articleAndReferenceDao.getAllArticleFull()) {
            Log.d("ARTICLEINFO"," Article is ${d.article.content} ID is ${d.article.id} "  
                    "\n\tMain Reference is ${d.main_reference.other_data} ID is ${d.main_reference.id}"  
                    "\n\tSecondary  Reference is ${d.other_reference.other_data} ID is ${d.other_reference.id}")
        }  
    }
}

Running the above results in the log containing :-

D/ARTICLEINFO:  Article is Content for Article1 ID is 1 
        Main Reference is Reference1 ID is 1
        Secondary  Reference is Reference2 ID is 2
D/ARTICLEINFO:  Article is Content for Article2 ID is 2 
        Main Reference is Reference3 ID is 3
        Secondary  Reference is Reference4 ID is 4
D/ARTICLEINFO:  Article is Content for Article 3 ID is 3 
        Main Reference is Reference5 ID is 5
        Secondary  Reference is reference6 ID is 6

Additional

You can also use @Embedded for all three parts.

The advantages are :-

  • more flexible filtering i.e. you can filter on the children (with @Relationship although you can, you need to defines the JOIN's)
  • Instead of multiple underlying queries for an @Relationship a single query retrieves all data

The disadvantages are:-

  • more complex query
  • requirement to use @ColumnInfo's prefix = annotation if column names are not unique to disambiguate them and thus more complex query to name the output columns accordingly.

So you could have :-

data class ArticleFullAlternative(

    @Embedded
    val article: Article,
    @Embedded(prefix = "main_")
    val main_reference: Reference,
    @Embedded(prefix = "other_")
    val other_reference: Reference
)

Along with an @Query such as :-

@Query("SELECT article.*, "  
        /* as prefix = "main_" has been used then rename output columns accordingly */
        "m.id AS main_id, m.other_data AS main_other_data, "  
        /* as prefix = "other_" has been used then rename output columns accordingly */
        "o.id AS other_id, o.other_data AS other_other_data "  
        "FROM article "  
        "JOIN reference AS m /*<<<<< to disambiguate column names */ ON main_reference_id = m.id "  
        "JOIN reference AS o /*<<<<< to disambiguate column names */ ON main_reference_id = o.id ")
abstract fun getArticleFullAlternative(): List<ArticleFullAlternative>

An example use in an Activity could be :-

    for(afa: ArticleFullAlternative in articleAndReferenceDao.getArticleFullAlternative()) {
        Log.d("ALTARTICLEINFO"," Article is ${afa.article.content} ID is ${afa.article.id} "  
                "\n\tMain Reference is ${afa.main_reference.other_data} ID is ${afa.main_reference.id}"  
                "\n\tSecondary  Reference is ${afa.other_reference.other_data} ID is ${afa.other_reference.id}")
    }
  • This produces exactly the same output
  • Related