使用带有 Android 的 Room 使用 UUID 作为主键
Posted
技术标签:
【中文标题】使用带有 Android 的 Room 使用 UUID 作为主键【英文标题】:Using UUID for Primary Key using Room with Android 【发布时间】:2020-04-21 16:10:28 【问题描述】:我需要将我的 android 移动应用与云中的多租户数据库同步。我认为最好的方法是使用 UUID 作为表的主键。我刚开始使用 Room,想知道在哪里/如何做到这一点,并正在寻找 1)关于将 UUID 用作 Room 的 PK 的意见,以及 2)我将在哪里实现这个(在模型中初始化 id 列,因为这是使用@NonNull) 的唯一方法?
更新
我使用过 INT,但不相信这是正确的方法。我在 android 开发者网站上看到了一个 example,它显示了一个正在使用的 uuid。当然会帮助我的云同步程序,但会牺牲速度和空间。我正在考虑一种混合方法,其中 INT 用于 PK,但 UUID 作为附加字段。这样我就不必为 FK 复制每个 UUID。我做了一些建模,看起来混合方法将是完整 UUID 方法大小的 50%(仅 INT 将是 25%)。
有人遇到过同样的问题吗?您是否选择了一个方向(UUID、INT 或混合)并且您后悔或做得很好?
【问题讨论】:
UUID 通常应该可以用作主键。但是,为什么要使用 UUID 而不是更简单(和更小)的东西,例如自动增量编号? 当我在多租户数据库中将数据同步回云端时,可以确保没有冲突。因此,如果我们在手机上有一个 Project 表,而在云中有一个 Project 表,则 ID 永远不会与另一个人的 Project 表 PK 相似。如果我在本地设备上使用 int 进行 PK,我将需要在云中使用不同的 PK。我在想如果本地有一个 UUID 作为 PK,它会变得如此简单。我不必担心多租户环境。 【参考方案1】:您必须使用 UUID 而不是 int 作为主键有几个原因。例如,您的应用程序是一个分布式应用程序,因为许多人使用许多设备编辑一个文档,并且您将记录每个人的编辑日志以用于版本控制功能。如果您的应用程序是集中式的(大多数情况下),最好使用 int,或者从服务器请求主键(可以是 int、long 或其他)。如果您必须使用 UUID,请使用它。否则 int 或 long 是更好的选择。
最后我会附上一个使用UUID作为android Room主键的例子。
UUID 存储空间和计算速度不应该是首要考虑的,除非你遇到过这些问题(不需要过度优化)。例如:一张图片的存储空间可能在1MB左右,UUID以String的形式保存在数据库中,采用utf-8格式,最多144 Byte(int为4 Byte),约为1/7000的一张照片。在服务器端,不推荐使用 UUID 作为主键,因为服务器同时服务上万用户,同时查询的次数很多。手机CPU虽然远弱于服务器CPU,但一次只为一个用户服务。同样,在必须使用 UUID 的场景下,也无需提前考虑优化问题。
书籍类
@Entity(tableName = "books")
public class Book
@PrimaryKey
@NonNull
private UUID id;
private String title;
private Date date;
public Book()
this.id = UUID.randomUUID();
date = new Date();
public UUID getId()
return id;
public void setId(UUID id)
this.id = id;
public String getTitle()
return title;
public void setTitle(String title)
this.title = title;
public Date getDate()
return date;
public void setDate(Date date)
this.date = date;
AppDatabase.class
@Database(entities = Book.class, version = 1, exportSchema = false)
@TypeConverters(UUIDConverter.class, DateConverter.class)
public abstract class AppDatabase extends RoomDatabase
private static AppDatabase INSTANCE;
private static final String DATABASE_NAME = "BookDatabase";
public abstract BookDao bookDao();
public static void init(Context context)
if (INSTANCE == null)
INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME).build();
public static AppDatabase getINSTANCE()
if (INSTANCE == null) throw new IllegalStateException("init in MyApplication.class");
return INSTANCE;
转换器:转换器将 uuid 或 sqlite 不支持的其他格式转换为支持类型。
public class UUIDConverter
@TypeConverter
public static String fromUUID(UUID uuid)
return uuid.toString();
@TypeConverter
public static UUID uuidFromString(String string)
return UUID.fromString(string);
public class DateConverter
@TypeConverter
public static long timestampFromDate(Date date)
return date.getTime();
@TypeConverter
public static Date dateFromTimestamp(long timestamp)
return new Date(timestamp);
【讨论】:
这很有帮助。谢谢你。使用 INT 确实成为外键引用的挑战。它最终会在不使用 UUID 的情况下进行大量的同步查找。这段代码非常有用且清晰。谢谢你。你在生产环境中使用过这个吗? 当你在android studio中想要取回id时会发生什么?通常在 Android 中插入时,您会返回 rowid。如何在插入时恢复 uuid? 很高兴我的回答对您有所帮助。当然我们在生产环境中使用 UUID。我们的应用程序在很多地方都使用了UUID,比如一些下载资源的标识符。在我看来,我们项目中的某些地方甚至有点滥用 UUID。例如,在许多地方自动递增 int 更好,我们仍然使用 UUID 代替。我主要负责Android客户端的工作,至少到目前为止还没有发现UUID导致的性能问题。服务器端不是我的责任,但我没有听说过 UUID 引起的明显性能问题。@lcj 自动增量 int 由 sqlite 创建并返回到您的代码。 UUID 由您创建,并保存到 sqlite。在将其保存到数据库之前,您已经知道它。或者它由服务器端创建。然后您将创建一个 Web API 以将 UUID 获取到客户端,然后将其保存到 sqlite。例如从服务器获取本周最近上传的 10 本书。服务器返回 10 个简单数据,包括 10 个 UUID 和书籍摘要。然后您可以使用 UUID 从服务器获取每本书的详细数据。希望这对您有所帮助。 @lcj 我从整数开始并一直使用它们,因为我担心外键和支持它们的 UUID 的扩散。我碰巧在数据库中有很多 FK。我真的不知道这是否是正确的调用,但从服务器端看来,GUID 显然不是一个好方法。除了 FK,人们似乎在性能和页面大小方面都面临挑战。我确实在某些表上使用 GUID 来唯一标识一条记录,但不是作为 PK。这种方法的缺点是需要大量查找以确保我有正确的记录,我真的不喜欢,但是....【参考方案2】:UUID 比使用 rowid 的别名更难,效率更低。您只需要使用column_name INTEGER PRIMARY KEY
说列,列是rowid 的别名。
如果没有给出值,那么 SQLite 会生成一个整数值。值最高可达 9223372036854775807(您也可以使用负数,这样可能会发生 2 次)。
使用 rowid 最多可以快 2 倍。
你可以赋值,它必须是整数。
问题是云中的唯一性。如果 App 没有赋值,那么没问题。
如果你想要随机性,那么如果不使用自动生成,SQlite 将生成一个唯一的随机值(在实体中只是 @PrimaryKey 和 Long 不长)并且使用 rowid 9223372036854775807(当达到时 SQLite 找到随机值)。
如果需要正负随机性,则添加带有 9223372036854775807 的行添加负值。
这就是你如何在房间里简单地制作它
实体喜欢
@Entity
public class RandomId
@PrimaryKey
Long randomId;
String name;
public RandomId()
public Long getRandomId()
return randomId;
public void setRandomId(Long randomId)
this.randomId = randomId;
public String getName()
return name;
public void setName(String name)
this.name = name;
道喜欢
@Dao
public interface RandomIdDao
@Insert
long insert(RandomId randomId);
@Query("SELECT * FROM randomid ORDER BY name")
List<RandomId> getAll();
@Database(version = 1,entities = RandomId.class)
public abstract class RandomIdDatabase extends RoomDatabase
abstract RandomIdDao randomIdDao();
活动喜欢
public class MainActivity extends AppCompatActivity
RandomIdDatabase randomIdDatabase;
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
randomIdDatabase = Room.databaseBuilder(this,RandomIdDatabase.class,"randomiddb")
.allowMainThreadQueries()
.addCallback(new RoomDatabase.Callback()
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db)
super.onCreate(db);
/* SETUP FOR positive and negative randomids */
ContentValues cv = new ContentValues();
cv.put("randomId",9223372036854775807L);
db.insert("RandomID", OnConflictStrategy.IGNORE,cv);
cv.clear();
cv.put("RandomId",-9223372036854775808L);
db.insert("RandomID", OnConflictStrategy.IGNORE,cv);
)
.build();
randomIdDatabase.getOpenHelper().getWritableDatabase().beginTransaction();
RandomId r = new RandomId();
for (int i = 0; i < 1000; i++)
r.setRandomId(null);
r.setName("name" + String.valueOf(i));
randomIdDatabase.randomIdDao().insert(r);
randomIdDatabase.getOpenHelper().getWritableDatabase().setTransactionSuccessful();
randomIdDatabase.getOpenHelper().getWritableDatabase().endTransaction();
List<RandomId> randomIds = randomIdDatabase.randomIdDao().getAll();
for (RandomId randomId : randomIds)
Log.d("IDDATA","name is " + randomId.getName() + " ID is " + randomId.getRandomId());
运行方式
D/IDDATA: name is null ID is -9223372036854775808 D/IDDATA: name is null ID is 9223372036854775807 D/IDDATA: name is name0 ID is 4107292993476813590 D/IDDATA: name is name1 ID is 22838362508669948 D/IDDATA: name is name10 ID is 3903077096050951194 D/IDDATA: name is name100 ID is 948211817704090341 D/IDDATA: name is name101 ID is 806075887185875254 D/IDDATA: name is name102 ID is 3204631877439831564 D/IDDATA: name is name103 ID is 1003770814770374644 D/IDDATA: name is name104 ID is 4116052162843098839 D/IDDATA: name is name105 ID is 3379736427023998848 D/IDDATA: name is name106 ID is 3597159533635558763 D/IDDATA: name is name107 ID is 3199952401843656135 D/IDDATA: name is name108 ID is 474496762820166420 D/IDDATA: name is name109 ID is 486041123921960369 D/IDDATA: name is name11 ID is 1162315037045732434 D/IDDATA: name is name110 ID is 2807178719405346105 D/IDDATA: name is name111 ID is 3643686187723794393 D/IDDATA: name is name112 ID is 4551641574908625814 D/IDDATA: name is name113 ID is 482870610003853013 D/IDDATA: name is name114 ID is 3865465837800377065 D/IDDATA: name is name115 ID is 2934767897308599664 D/IDDATA: name is name116 ID is 4473965121303816092 D/IDDATA: name is name117 ID is 2244323676217441072 D/IDDATA: name is name118 ID is 911318293674111436 D/IDDATA: name is name119 ID is 1434915280093930897 D/IDDATA: name is name12 ID is 2661856341619735016 ........
【讨论】:
这很有帮助。谢谢你。我没有想到这一点。我仍然担心冲突,特别是在人们可能对其数据敏感的多租户环境中。关于 Activity 中的工作,我认为这将被封装在实体类本身中,因此只需完成一次工作。我在使用 UUID 时正在考虑这一点,或者 1)在实例化类时生成它(解决 @NonNull 问题)或 2)在插入之前设置它。以上是关于使用带有 Android 的 Room 使用 UUID 作为主键的主要内容,如果未能解决你的问题,请参考以下文章
如何将带有 Android Room 的应用添加为使用 makefile 编译的系统应用?