avatarPaolo Montalto

Summary

The provided web content discusses the implementation of Universally Unique Identifiers (UUIDs) as primary keys in a Room Database, as an alternative to the standard auto-generated Integer IDs.

Abstract

The article outlines the challenges of using integer-based primary keys in a Room Database, particularly in scenarios involving data synchronization across multiple devices or remote data storage. It proposes the use of UUIDs to avoid ID collisions and simplify data management. The author explains the necessary steps to integrate UUIDs into Room entities, including defining a default UUID value and using TypeConverters to handle UUID conversion to and from strings, as SQLite does not natively support UUIDs. The article concludes with a sample implementation of a User entity with a UUID primary key and the corresponding changes to the Room database setup, demonstrating how Room can be configured to work seamlessly with UUIDs.

Opinions

  • The author suggests that integer IDs, while simple, are not ideal for multi-device synchronization or remote data storage due to the complexity of handling collisions.
  • UUIDs are presented as a superior alternative for primary keys in certain use cases, particularly for avoiding ID conflicts across different data sources.
  • The author provides a personal account of implementing UUIDs in Room, implying that this approach has been beneficial in their experience.
  • The article invites discussion and shared experiences from other developers, indicating an openness to different perspectives and solutions in the community.

Universally Unique IDs as a Primary Key in a Room Database

The default choice for primary key identifiers in a Room Database is usually an auto-generated Integer field, e.g. :

@PrimaryKey(autoGenerate = true) val id: Int

Integer IDs are plain and straightforward to implement but not so easy to handle when it comes to apps wich can run on multiple devices at the same time or when you have remotely stored data with all its consequences (e.g. coping with collisions is a literal hell)

In many cases like the foresaid ones having Universally Unique Identifiers maight come in handy.

Here follows a brief summary of how I managed to have UUIDs as primary keys in my Room Database based apps.

Let’s assume we have a simple Room entity we use to store our app’s users, like the one below:

@Entity
data class User(
  @PrimaryKey(autoGenerate = true) val id: Int = 0,
  val firstName: String,
  val lastName: String
)

Room will auto-generate and auto-increment user IDs when you save it to the database.

Now let’s see how to achieve the same with UUIDs.

First of all we will need to convert our id to an UUID, like this:

@Entity
data class User(
  @PrimaryKey val id: UUID,
  val firstName: String,
  val lastName: String
)

But, since we don’t have a UUID field type in SQLite we will need to put some measures in place.

In the classic method we had a default zero value for our integer id, we’ll do the same for UUIDs, e.g.

@Entity(tableName = "users")
data class User(
  @PrimaryKey val id: UUID = UUID.fromString(DEFAULT_UUID),
  val firstName: String,
  val lastName: String
) {

    companion object {
        const val DEFAULT_UUID = "00000000-0000-0000-0000-000000000000"
    }

}

Now that we have our entity let’s see how to define the app’s database.

Usually you would do so

@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

Where UserDao can be defined as follows

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAll() : List<User>

    @Query("SELECT * FROM users WHERE id = :id")
    fun getById(id: UUID) : User?

    @Insert
    fun insert(user: User)

    @Update
    fun update(user: User)

    @Delete
    fun delete(user: User)
}

But then again, since UUIDs are not natively supported we will need to find our solution.

Here’s where TypeConverters come in handy.

We can define a proper TypeConverter to let Room convert UUIDs to/from strings

class Converters {
    
    @TypeConverter
    fun fromUUID(uuid: UUID): String? {
        if ( uuid.toString() == User.DEFAULT_UUID )
            return UUID.randomUUID().toString()

        return uuid.toString()
    }

    @TypeConverter
    fun uuidFromString(string: String?): UUID {
        string?.let {
            return UUID.fromString(string)
        }
        return UUID.fromString(User.DEFAULT_UUID)
    }

}

As you can see the “fromUUID” Type Converter will generate a new UUID for User instances with default identifier.

In order for these TypeConverters to do their job you will need to add it to the AppDatabase like this

@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

Once you did so, Room will automatically convert UUIDs to/from Strings making it all work seamlessly.

val db = Room.databaseBuilder(
  context, AppDatabase::class.java, "example-database"
).build()

val dao = db.userDao()
val user = User(firstName = "Paolo", lastName = "Montalto")

dao.insert(user)

This is just a sample implementation, anyone can of course find the most suitable for own needs.

Your experience and opinions are welcome in the comments.

Android
Android App Development
Room
Database
Mobile App Development
Recommended from ReadMedium