Understand GemAI’s data persistence with Room Database, repository pattern, DAOs, entities, and data flow
The data layer in GemAI handles all data persistence and retrieval operations using Room Database, DataStore, and the repository pattern. This layer is completely separate from the UI and provides a clean API for the domain layer.
Entities define the database schema and are annotated with Room annotations:
@Entity(tableName = "conversations")data class ConversationEntity( @PrimaryKey(autoGenerate = true) val id: Long = 0, val timestamp: Long, val title: String?, val lastMessageTimestamp: Long, val isUsedForPromptSuggestions: Boolean = false,)
Notice the ForeignKey constraint on MessageEntity. When a conversation is deleted, all associated messages are automatically deleted (CASCADE).
@Daointerface ConversationDao : BaseDao<ConversationEntity> { @Query("SELECT * FROM conversations") fun getConversations(): Flow<List<ConversationEntity>> @Query("SELECT * FROM conversations WHERE id = :id") suspend fun getConversationById(id: Long): ConversationEntity @Query("UPDATE conversations SET title = :title WHERE id = :conversationId") suspend fun updateTitle(conversationId: Long, title: String) @Query("DELETE FROM conversations WHERE id = :conversationId") suspend fun deleteConversation(conversationId: Long) @Query("UPDATE conversations SET isUsedForPromptSuggestions = :isUsed WHERE id = :conversationId") suspend fun setUsedForPromptSuggestions(conversationId: Long, isUsed: Boolean) @Query("SELECT conversations.id FROM conversations WHERE isUsedForPromptSuggestions = 0") suspend fun getConversationsForPromptSuggestions(): List<Long>}
@Daointerface MessageDao : BaseDao<MessageEntity> { @Query("SELECT * FROM messages WHERE conversationId = :conversationId") fun getMessagesForConversation(conversationId: Long): Flow<List<MessageEntity>> @Query("SELECT * FROM messages WHERE conversationId = :conversationId") suspend fun getMessages(conversationId: Long): List<MessageEntity> @Query("SELECT * FROM messages WHERE conversationId = :conversationId AND participant = :participant") suspend fun getParticipantMessages( conversationId: Long, participant: Participant = Participant.USER, ): List<MessageEntity> @Query("SELECT * FROM messages WHERE id = :id") suspend fun getMessageById(id: Long?): MessageEntity? @Query("UPDATE messages SET status = :status WHERE id = :id") suspend fun updateMessageStatus(id: Long, status: MessageStatus) @Query("UPDATE conversations SET lastMessageTimestamp = :timestamp WHERE id = :conversationId") suspend fun updateLastMessageTimestamp(conversationId: Long, timestamp: Long) @Transaction suspend fun addMessageToConversation(message: MessageEntity): Long { val id = insert(message) updateLastMessageTimestamp(message.conversationId, message.timestamp) return id } @Transaction suspend fun insertOrUpdate( id: Long?, insert: suspend () -> Unit, update: suspend (id: Long, messageContent: String) -> Unit, ) { if (id == null) { insert() return } val message = getMessageById(id) if (message == null) { insert() } else { update(id, message.content) } } @Query("SELECT COUNT(*) FROM messages WHERE conversationId = :conversationId") suspend fun getMessageCount(conversationId: Long): Int}
Understanding @Transaction methods
The @Transaction annotation ensures that multiple database operations execute atomically:
addMessageToConversation(): Inserts a message and updates the conversation’s last message timestamp in a single transaction
insertOrUpdate(): Checks if a message exists and either inserts or updates accordingly
If any operation fails, all changes are rolled back.