如何在Android Room中使用唯一索引来执行Upsert操作?

huangapple go评论57阅读模式
英文:

How to Upsert item with unique index in Android Room?

问题

I have translated the code sections as requested:

我有像这样的API主机中的JSON数据

```json
   "product": {
      "barcode": "2489752310342",
      "productName": "Trench coat",
      "priceList": [
        {
          "price": 345
        },
        {
          "price": 123
        }
      ]
    }

我以这种方式获取数据并将其写入SQLite与Android Room数据库一起使用。我有Product和Price实体,它们之间存在一对多的关系。我将数据存储在Product Entity的priceList中,在Price Entity中(我知道可以使用Type Converters的帮助将其存储为Product中的字符串类型列,但我的经理要求我将其存储在另一个Entity中)。

Product.java

@Entity(tableName = "product", indices = {@Index(value = {"barcode"},
        unique = true)})
public class Product {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    private int id;

    @ColumnInfo(name = "barcode")
    private String barcode;

    @ColumnInfo(name = "product_name")
    private String productName;

    @ColumnInfo(name = "price_list")
    @Ignore
    private ArrayList<Price> priceList;
}

Price.java

@Entity(tableName = "price",
        foreignKeys = @ForeignKey(
                entity = Product.class,
                parentColumns = "id",
                childColumns = "product_id",
                onDelete = ForeignKey.CASCADE,
                onUpdate = ForeignKey.CASCADE
        ))
public class Price {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    private int id;

    @ColumnInfo(name = "product_id")
    private int productId;

    @ColumnInfo(name = "price")
    private int price;
}

由于在这些之间定义关系需要产品ID,所以在从API获取数据后,我会像这样更新产品到数据库中

private void upsertProduct(Product product){
    int insertedRowId = (int) productDao.upsertData(product);
    product.setId(insertedRowId);
    addPricesDependOnProductId(product);
}

在为Product分配ID之后,我会将此ID分配给Price的产品ID,如下所示

private void addPricesDependOnProductId(Product product) {
    for (Price price : product.getPriceList()) {
        price.setProductId(product.getId());
        priceDao.upsertPrice(price);
    }
}

这运行得很好,我可以使用中间数据类获取Price和Product数据。

完成这些操作后,我的经理要求我删除Price表中的所有行,并在不更改Product项的ID的情况下再次插入它们。

我删除了Price表中的所有项目,然后再次从API获取数据。但是我遇到了“UNIQUE constraint failed: product.barcode (code 2067 SQLITE_CONSTRAINT_UNIQUE)”错误。据我了解,Room抛出此异常,因为我尝试插入已经存在的条形码,这必须是唯一的。

ProductDao.java

@Dao
public interface ProductDao {
    @Upsert
    long upsertData(Product product);
}

PriceDao.java

@Dao
public interface PriceDao {
    @Query("DELETE FROM price")
    void deleteAllPrices();

    @Upsert
    void upsertPrice(Price price);
}

我期望如果Product表中已经存在一个Product项,我将得到它的ID,将这个ID分配给Price项的产品ID,并在不增加Product的主键的情况下写入DB。是否可以在不增加Product的ID的情况下执行此操作?

SOLUTION:

我在ProductDao.java中添加了此方法

@Query("SELECT * FROM product WHERE barcode = :barcode")
Product getProductByBarcode(String barcode);

并将我的方法更新为

private void upsertProduct(Product product){
        String barcode = product.getBarcode();
        new Thread(() -> {
            Product productInDB = productDao.getProductByBarcode(barcode);
            if (productInDB == null) {
                // If there is no product with given unique barcode in DB insert and get id of it.
                int insertedRowId = (int) productDao.upsertData(product);
                // After inserting the product in DB, assign its id to the product object that I've got from API call.
                product.setId(insertedRowId);
                addPricesDependOnProductId(product);
            } else {
                // If product item already exists in DB fetch from the DB and set product's price list to it.
                productInDB.setPriceList(product.getPriceList());
                addPricesDependOnProductId(productInDB);
            }
        }).start();
}
英文:

I have JSON data in an API host like this

   &quot;product&quot;: {
      &quot;barcode&quot;: &quot;2489752310342&quot;,
      &quot;productName&quot;: &quot;Trench coat&quot;,
      &quot;priceList&quot;: [
        {
          &quot;price&quot;: 345
        },
        {
          &quot;price&quot;: 123
        }
      ]
    }

I'm fetching data in this form and writing to SQLite with Android Room database. I have Product and Price entities and there is a one-to-many relationship between them. I'm storing the data in Product Entity's priceList in Price entity (I know I can store this as a column in Product with string type with the help of Type Converters but my manager wanted me to store it in another Entity).

Product.java

@Entity(tableName = &quot;product&quot;, indices = {@Index(value = {&quot;barcode&quot;},
        unique = true)})
public class Product {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = &quot;id&quot;)
    private int id;

    @ColumnInfo(name = &quot;barcode&quot;)
    private String barcode;

    @ColumnInfo(name = &quot;product_name&quot;)
    private String productName;

    @ColumnInfo(name = &quot;price_list&quot;)
    @Ignore
    private ArrayList&lt;Price&gt; priceList;
}

Price.java

@Entity(tableName = &quot;price&quot;,
        foreignKeys = @ForeignKey(
                entity = Product.class,
                parentColumns = &quot;id&quot;,
                childColumns = &quot;product_id&quot;,
                onDelete = ForeignKey.CASCADE,
                onUpdate = ForeignKey.CASCADE
        ))
public class Price {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = &quot;id&quot;)
    private int id;

    @ColumnInfo(name = &quot;product_id&quot;)
    private int productId;

    @ColumnInfo(name = &quot;price&quot;)
    private int price;
}

Since defining a relationship between these requires a product id, after I fetch data from API I Upsert Product to the database like this

private void upsertProduct(Product product){
    int insertedRowId = (int) productDao.upsertData(product);
    product.setId(insertedRowId);
    addPricesDependOnProductId(product);
}

After I assign an id to Product, I assign this id to Price product id like this

private void addPricesDependOnProductId(Product product) {
    for (Price price : product.getPriceList()) {
        price.setProductId(product.getId());
        priceDao.upsertPrice(price);
    }
}

This works like a charm and I can get Price and Product data using the intermediate data class.

After these things are done my manager wanted me to do delete all the rows in the Price table and insert them again without changing the ids of Product items.

I deleted all the items in the Price table and fetch the data from API again. But I faced "UNIQUE constraint failed: product.barcode (code 2067 SQLITE_CONSTRAINT_UNIQUE)" error. As I understand room throws this exception because I tried to insert an already existing barcode which must be unique.

ProductDao.java

@Dao
public interface ProductDao {
    @Upsert
    long upsertData(Product product);
}

PriceDao.java

@Dao
public interface PriceDao {
    @Query(&quot;DELETE FROM price&quot;)
    void deleteAllPrices();

    @Upsert
    void upsertPrice(Price price);

}

I'm expecting if a Product item already exists in the Product table I'll get the id of it, assign this id to the Price item's product id and write to DB without increasing the primary key of the Product (Product's product id must be the same as the before deleting all the items in the Price table). Is it possible to do this without increase id of the Product?

SOLUTION:

I added this method in ProductDao.java

@Query(&quot;SELECT * FROM product WHERE barcode = :barcode&quot;)
Product getProductByBarcode(String barcode);

And updated my method like this

private void upsertProduct(Product product){
        String barcode = product.getBarcode();
        new Thread(() -&gt; {
            Product productInDB = productDao.getProductByBarcode(barcode);
            if (productInDB == null) {
                // If there is no product with given unique barcode in DB insert and get id of it.
                int insertedRowId = (int) productDao.upsertData(product);
                // After inserting the product in DB, assign its id to the product object that I&#39;ve got from API call.
                product.setId(insertedRowId);
                addPricesDependOnProductId(product);
            } else {
                // If product item already exists in DB fetch from the DB and set product&#39;s price list to it.
                productInDB.setPriceList(product.getPriceList());
                addPricesDependOnProductId(productInDB);
            }
        }).start();
}

答案1

得分: 1

以下是您要翻译的内容:

Is it possible to do this without increase id of Product?

Yes. However :-

Room的@Upsert是一个方便的方法,只处理典型/常见/基本的用法。因此,要满足多个唯一约束的更复杂性质,您可以在@Dao中创建一个方法,有效地模仿Upsert。

这将需要具有方法主体的方法,因此@Dao是一个抽象类,而不是接口。

为了简洁起见,这是一个@Dao抽象类,包含所有DAO(请注意,Price Daos未在演示中使用):-

@Dao
abstract class AllDAOs {
    @Upsert
    abstract long upsertData(Product product);
    @Query("DELETE FROM price")
    abstract void deleteAllPrices();
    @Upsert
    abstract void upsertPrice(Price price);

    /* Query to return the number of matching rows according to barcode or id.
        should only return 0 (no row) or 1 (matched)
     */
    @Query("SELECT count() FROM product WHERE id=:id OR barcode LIKE :barcode")
    abstract int ifProductExists(int id, String barcode);
    /* just in case the product name is changed then allow updating of the product */
    @Query("UPDATE product SET product_name=:productName WHERE barcode LIKE :barcode")
    abstract int updateProduct(String productName, String barcode);

    /* The equivalent of the Upsert that caters with the two UNIQUE indexes */
    @Transaction
    @Query("")
    long /* always want to return the id */ upsertProduct(Product product) {
        /* If the Product exists then UPDATE the product name of the existing row */
        if (ifProductExists(product.getId(),product.getBarcode()) > 0) {
            updateProduct(product.getProductName(), product.getBarcode());
            return product.getId();
        } else {
            /* If the Product does not exist then insert it using the Upsert (as that exists)
                really it would be more correct to use an `@Insert`
             */
            return upsertData(product);
        }
    }
}

Demo

以下演示使用upsertProduct方法多次插入相同的Product,而不会失败,并且在第一次插入后还更新了Product的名称(未指定名称是否会更改,因此以防万一包含了此功能)。

以下是活动代码(注意,.alloMainThreadQueries用于简洁和方便):-

public class MainActivity extends AppCompatActivity {

    TheDatabase db;
    AllDAOs dao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        db = TheDatabase.getInstance(this);
        dao = db.geAllDAOs();

        Product prd1 = new Product();
        prd1.setBarcode("2489752310342");
        prd1.setProductName("Trench coat");
        prd1.setPriceList(new ArrayList<Price>());
        logInsertProduct(prd1);
        prd1.setProductName("Updated Trench Coat");
        logInsertProduct(prd1);
        logInsertProduct(prd1);

    }

    void logInsertProduct(Product product) {
        String result = "Successful";
        long rowidOfInsert = dao.upsertProduct(product);
        if (rowidOfInsert < 0) result = "Unsuccessful";
        Log.d("UPSERTPRODUCT","The result of insert product " + product.getProductName() + " was " + result + "rowid returned was " + rowidOfInsert);
    }
}

Demo Results

运行后,product表如下:-

如何在Android Room中使用唯一索引来执行Upsert操作?

即,根据第一个upsertProductid为1,但根据第2和第3个upsertProduct,名称已更改。

而日志包括:-

2023-05-30 18:13:17.325 D/UPSERTPRODUCT: The result of insert product Trench coat was Successfulrowid returned was 1
2023-05-30 18:13:17.331 D/UPSERTPRODUCT: The result of insert product Updated Trench Coat was Successfulrowid returned was 0
2023-05-30 18:13:17.346 D/UPSERTPRODUCT: The result of insert product Updated Trench Coat was Successfulrowid returned was 0

另一种选择是将条形码作为主键,但这样做效率较低,因为即使SQLite将该值存储为数字(如果我没记错的话),由于列的类型为TEXT,它不会是更高效的rowid的别名(可以快两倍)。

甚至更冒险,更复杂的选择是删除条形码上的索引,执行upsert,然后创建索引。

英文:

> Is it possible to do this without increase id of Product?

Yes. However:-

Room's @Upsert is a convenience method and only copes with typical/common/basic usage. As such to cater for the more complex nature of multiple UNIQUE constraints then you could have a method in an @Dao that effectively mimics an Upsert.

This would require methods that have bodies and thus an @Dao that is an abstract class as opposed to an interface.

For brevity here's an @Dao abstract class, that embodies all the DAOs (not that the Price Daos are used in the demo):-

@Dao
abstract class AllDAOs {
    @Upsert
    abstract long upsertData(Product product);
    @Query(&quot;DELETE FROM price&quot;)
    abstract void deleteAllPrices();
    @Upsert
    abstract void upsertPrice(Price price);

    /* Query to return the number of matching rows according to barcode or id.
        should only return 0 (no row) or 1 (matched)
     */
    @Query(&quot;SELECT count() FROM product WHERE id=:id OR barcode LIKE :barcode&quot;)
    abstract int ifProductExists(int id, String barcode);
    /* just in case the product name is changed then allow updating of the product */
    @Query(&quot;UPDATE product SET product_name=:productName WHERE barcode LIKE :barcode&quot;)
    abstract int updateProduct(String productName, String barcode);

    /* The equivalent of the Upsert that caters with the two UNIQUE indexes */
    @Transaction
    @Query(&quot;&quot;)
    long /* always want to return the id */ upsertProduct(Product product) {
        /* If the Product exists then UPDATE the product name of the existing row */
        if (ifProductExists(product.getId(),product.getBarcode()) &gt; 0) {
            updateProduct(product.getProductName(), product.getBarcode());
            return product.getId();
        } else {
            /* If the Product does not exist then insert it using the Upsert (as that exists)
                really it would be more correct to use an `@Insert`
             */
            return upsertData(product);
        }
    }
}

Demo

The following demo uses the upsertProduct method to insert the same Product 3 times, without failing and also after the first also updates the Product's name (it is not specified whether or not the name could change, so just in case this feature was included).

The following is the activity code (note .alloMainThreadQueries has been used for brevity and convenience):-

public class MainActivity extends AppCompatActivity {

    TheDatabase db;
    AllDAOs dao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        db = TheDatabase.getInstance(this);
        dao = db.geAllDAOs();

        Product prd1 = new Product();
        prd1.setBarcode(&quot;2489752310342&quot;);
        prd1.setProductName(&quot;Trench coat&quot;);
        prd1.setPriceList(new ArrayList&lt;Price&gt;(){});
        logInsertProduct(prd1);
        prd1.setProductName(&quot;Updated Trench Coat&quot;);
        logInsertProduct(prd1);
        logInsertProduct(prd1);

    }

    void logInsertProduct(Product product) {
        String result = &quot;Successful&quot;;
        long rowidOfInsert = dao.upsertProduct(product);
        if (rowidOfInsert &lt; 0) result = &quot;Unsuccessful&quot;;
        Log.d(&quot;UPSERTPRODUCT&quot;,&quot;The result of insert product &quot; + product.getProductName() + &quot; was &quot; + result + &quot;rowid returned was &quot; + rowidOfInsert);
    }
}

Demo Results

After running the product table:-

如何在Android Room中使用唯一索引来执行Upsert操作?

i.e. the id is 1 as per the initial upsertProduct, but the name has changed according to the 2nd and 3rd upsertProduct

And the Log includes:-

2023-05-30 18:13:17.325 D/UPSERTPRODUCT: The result of insert product Trench coat was Successfulrowid returned was 1
2023-05-30 18:13:17.331 D/UPSERTPRODUCT: The result of insert product Updated Trench Coat was Successfulrowid returned was 0
2023-05-30 18:13:17.346 D/UPSERTPRODUCT: The result of insert product Updated Trench Coat was Successfulrowid returned was 0

Another alternative would be to use the barcode as the primary key, but that then is less efficient as even though SQLite will store the value as if it were a number (iirc) as the column would be of type TEXT, it would not be an alias of the more efficient rowid (which can be up to twice as fast).

An even riskier, and more complex alternative could be to drop the index on the barcode, do the upsert and then create the index.

huangapple
  • 本文由 发表于 2023年5月30日 05:17:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/76360363.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定