使用带有 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 编译的系统应用?

使用 Android ORM Room 的最佳方式?

Android Room - 带有附加字段的多对多关系

Android Room:按不工作排序

如何高效查询带有两个 WHERE 子句的表(Android Room)

Android Room LiveData观察器未更新