【问题标题】:Android Room - How can I check if an entity with the same name already exists before inserting?Android Room - 如何在插入之前检查同名实体是否已存在?
【发布时间】:2020-06-11 21:31:54
【问题描述】:

我正在使用带有 android room 的 mvvm 模式创建一个应用程序,但在验证用户输入时遇到了一些问题。当用户想要向应用程序添加成分时,他们需要输入该成分的名称。如果名称已在使用中,我希望应用程序通知用户。我使用 Transformations.Map() 函数尝试了一些东西,但没有任何成功。

我对 mvvm 模式和 LiveData 还很陌生,而且我已经坚持了很长一段时间,所以任何建议都将不胜感激。

这是成分实体:

@Entity(tableName = "ingredient")
public class BaseIngredient {

@PrimaryKey(autoGenerate = true)
private int id;

private String name;

private String category;

@ColumnInfo(name = "cooking_time")
private int cookingTime;

@Ignore
public BaseIngredient() {
}

public BaseIngredient(int id, @NonNull String name, @NonNull String category, int cookingTime)
        throws InvalidValueException {
    this.id = id;
    setName(name);
    setCookingTime(cookingTime);
    setCategory(category);
}

public void setName(String name) throws InvalidNameException {
    if (name == null || name.isEmpty())
        throw new InvalidNameException("Name is empty");
    if (!name.matches("[A-z0-9]+( [A-z0-9]+)*"))
        throw new InvalidNameException("Name contains invalid tokens");

    this.name = name;
}

public void setCategory(String category) throws InvalidCategoryException {
    if (category == null || category.isEmpty())
        throw new InvalidCategoryException("Category is empty");
    if (!category.matches("[A-z0-9]+"))
        throw new InvalidCategoryException("Category contains invalid tokens");

    this.category = category;
}

public void setCookingTime(int cookingTime) throws InvalidCookingTimeException {
    if (cookingTime < 1)
        throw new InvalidCookingTimeException("Time must be positive");

    this.cookingTime = cookingTime;
}

/* getters */

public boolean isValid() {
    return name != null && category != null && cookingTime != 0;
}

这是我正在使用的成分库:

private IngredientDao ingredientDao;

private LiveData<List<BaseIngredient>> ingredients;

public IngredientRepository(Application application) {
    LmcfyDatabase database = LmcfyDatabase.getDatabase(application.getApplicationContext());
    ingredientDao = database.ingredientDao();
    ingredients = ingredientDao.getAllIngredients();
}

public LiveData<List<BaseIngredient>> getAllIngredients() {
    return ingredients;
}

public LiveData<List<BaseIngredient>> getIngredientsWithQuery(String query) {
    return ingredientDao.getIngredientsWithQuery("%" + query + "%");
}

public void insert(BaseIngredient ingredient) {
    LmcfyDatabase.databaseWriteExecutor.execute(() -> {
        ingredientDao.insert(ingredient);
    });
}

public LiveData<Integer> getIngredientsWithNameCount(String name) {
    return ingredientDao.getIngredientsWithNameCount(name);
}

成分道:

@Insert(onConflict = OnConflictStrategy.IGNORE, entity = BaseIngredient.class)
long insert(BaseIngredient ingredient);

@Delete(entity = BaseIngredient.class)
void delete(BaseIngredient ingredient);

@Query("SELECT * FROM ingredient")
LiveData<List<BaseIngredient>> getAllIngredients();

@Query("SELECT * FROM ingredient WHERE name LIKE :query")
LiveData<List<BaseIngredient>> getIngredientsWithQuery(String query);

@Query("SELECT COUNT(id) FROM ingredient WHERE name LIKE :name")
LiveData<Integer> getIngredientsWithNameCount(String name);

最后是用于创建成分的 ViewModel

    private final IngredientRepository repository;

private final BaseIngredient ingredient;

private final MutableLiveData<String> nameError;

private final MutableLiveData<String> categoryError;

private final MutableLiveData<String> cookingTimeError;

private final MutableLiveData<Boolean> ingredientValidStatus;

public AddIngredientViewModel(@NonNull Application application) {
    super(application);
    repository = new IngredientRepository(application);
    ingredient = new BaseIngredient();

    nameError = new MutableLiveData<>();
    categoryError = new MutableLiveData<>();
    cookingTimeError = new MutableLiveData<>();
    ingredientValidStatus = new MutableLiveData<>();
}

public void onNameEntered(String name) {
    try {
        ingredient.setName(name);
        nameError.setValue(null);
    } catch (InvalidNameException e) {
        nameError.setValue(e.getMessage());
    } finally {
        updateIngredientValid();
    }
}

public void onCategoryEntered(String category) {
    try {
        ingredient.setCategory(category);
        categoryError.setValue(null);
    } catch (InvalidCategoryException e) {
        categoryError.setValue(e.getMessage());
    } finally {
        updateIngredientValid();
    }
}

public void onCookingTimeEntered(int cookingTime) {
    try {
        ingredient.setCookingTime(cookingTime);
        cookingTimeError.setValue(null);
    } catch (InvalidCookingTimeException e) {
        cookingTimeError.setValue(e.getMessage());
    } finally {
        updateIngredientValid();
    }
}

private void updateIngredientValid() {
    ingredientValidStatus.setValue(ingredient.isValid());
}

public boolean saveIngredient() {
    if (ingredient.isValid()) {
        Log.d(getClass().getName(), "saveIngredient: Ingredient is valid");
        repository.insert(ingredient);
        return true;
    } else {
        Log.d(getClass().getName(), "saveIngredient: Ingredient is invalid");
        return false;
    }
}

viewmodel 中的 onXXEntered() 函数链接到片段中的 textViews,并且当按下保存按钮时调用 saveIngredient() 函数。 XXError LiveData 用于向用户显示错误。

真正的问题在于 LiveData 是异步的,用户可以在 LiveData 包含来自数据库的结果之前更改他们的输入并单击保存按钮。如果我想在保存时检查输入,则“添加活动”将在检查完成之前完成。

再次感谢任何帮助。

【问题讨论】:

    标签: java android mvvm android-room android-livedata


    【解决方案1】:

    在我最近的一个项目中,我不得不做类似的事情。我所做的是:

    1. Room 无法创建带有 SQLite Unique 约束的列(如果它不是 PrimaryKey - 这是你的情况)。所以不要在你的应用代码中使用 Room 初始化数据库。相反,在您的应用程序之外创建一个数据库文件。在 'name' 列上添加 Unique 约束。然后在 assets 文件夹下的项目中添加数据库文件。 (例如,在 assets 中创建一个子文件夹 - 'db_files' - 并将您预先创建的数据库文件复制到此文件夹下)

    2. 我猜你在 @DataBase 类中使用了单例模式。将您的“getInstance()”方法替换为以下内容:

    public static MyDB getInstance(final Context context) {
            if(INSTANCE == null) {
                synchronized (AVListDB.class) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                            MyDB.class,"myDB.db")
                            .createFromAsset( "db/myDB.db")
                            .build();
    
                }
            }
            return INSTANCE;
        }
    

    这会在应用程序数据库文件路径下创建预打包数据库文件的副本。

    1. 有了唯一约束,您的 @Insert@Update 注释方法将遵守约束条件,如果出现以下情况,将抛出 SQLiteConstraintException您尝试插入以前使用的名称。您可以捕获此异常,并根据需要将其传递给您的 View 或 ViewModel(我实现了一个简单的集中式事件发布器组件)。

    希望对你有帮助。

    【讨论】:

    • 创建所需唯一约束的更简单方法是让 Room 为列创建唯一索引:@Entity(tableName = "ingredient", indices = @Index(value = "name", unique = true))。描述于the documentation
    • @BobSnyder 感谢您提供的信息。您知道该功能是最近更新(去年内)还是更早版本?
    • 我不知道这个功能是什么时候实现的。我没有在the Room release notes 中找到它。
    • @BobSnyder 谢谢你们的回复。我当然会添加约束,但问题不会仍然存在吗?即使数据库不允许,异常仍然会被异步抛出。这意味着在 Activity 完成之前无法向用户显示异常,对吧?
    • 您可以从那里做的是从您的存储库发布事件 - 成功 dbase 事务时的“onSuccess()”或捕获异常时的“onInsertFailed()”。在两种方法之一返回之前不要完成您的活动。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多