Дополнение a записывает столбцы в комнате

#android #android-room

#Android #android-комната

Вопрос:

Я довольно новичок в Android Room и SQLite в целом, поэтому извините, если это простой вопрос.

Я получаю данные из API, которые я хотел бы вставить в базу данных, чтобы они были доступны, когда устройство находится в автономном режиме.

В зависимости от конечной точки API некоторые поля моих объектов данных могут быть нулевыми (подумайте о сводке только с основными полями по сравнению с полностью подробным объектом со всеми полями)

Чтобы сохранить базу данных в чистоте, я хотел бы обновить записи, но только те столбцы, которые не являются нулевыми (например, для которых у меня есть новые значения), а остальные столбцы оставить нетронутыми.

Вот несколько примеров классов для пояснения:

Человек

 @Entity(tableName = "person", indices = {
        @Index(value = "id", unique = true)
})
public class Person {
    @PrimaryKey
    public int id;

    public String name;

    public String description;
}
 

Пример:

 // create db
RoomDB db = RoomDB.create(ctx);

// create some sample objects
final Person p2 = new Person(2, "Peter", null);

// insert them into the db
db.personDao().insert(p2);

// create a updated peter that likes spiders
// but has no name (as a example)
final Person newPeter = new Person(2, null, "Peter likes spiders");

// and update him
db.personDao().updateNonNull(newPeter);

// now we read him back
final Person peter = db.personDao().getById(2);    
 

В этом примере желаемые значения ‘peter’ будут:

 id = 2
name = "Peter"
description = "Peter likes spiders"
 

Однако, используя @Update или @Insert в комнате, я могу получить только это:

 id = 2
name = null
description = "Peter likes spiders"
 

Единственный способ, который я нашел для достижения этой цели, — вручную получить объект и дополнить значения следующим образом:

 @Transaction
public void updateNonNull(Person newPerson) {
    final Person oldPerson = getById(newPerson.id);
    if (oldPerson == null) {
        insert(newPerson);
        return;
    }
    
    if (newPerson.name == null)
        newPerson.name = oldPerson.name;

    if (newPerson.description == null)
        newPerson.description = oldPerson.description;

    update(newPerson);
}
 

Однако это привело бы к довольно большому количеству кода с более крупными объектами…
Итак, мой вопрос, есть ли лучший способ сделать это?

Редактировать:

После некоторого тестирования с помощью SQL от @Priyansh Kedia я обнаружил, что эти функции действительно работают по назначению и делают это с более высокой производительностью, чем java.

Однако, поскольку для выполнения инструкции SQL мне потребовалось бы писать огромные запросы, я решил использовать решение, основанное на отражении, как показано ниже. Я сделал это только потому, что функция вызывается нерегулярно, поэтому более низкая производительность не будет иметь большого значения.

     /**
     * merge two objects fields using reflection.
     * replaces null value fields in newObj with the value of that field in oldObj
     * <p>
     * assuming the following values:
     * oldObj: {name: null, desc: "bar"}
     * newObj: {name: "foo", desc: null}
     * <p>
     * results in the "sum" of both objects: {name: "foo", desc: "bar"}
     *
     * @param type   the type of the two objects to merge
     * @param oldObj the old object
     * @param newObj the new object. after the function, this is the merged object
     * @param <T>    the type
     * @implNote This function uses reflection, and thus is quite slow.
     * The fastest way of doing this would be to use SQLs' ifnull or coalesce (about 35% faster), but that would involve manually writing a expression for EVERY field.
     * That is a lot of extra code which i'm not willing to write...
     * Besides, as long as this function isn't called too often, it doesn't really matter anyway
     */
    public static <T> void merge(@NonNull Class<T> type, @NonNull T oldObj, @NonNull T newObj) {
        // loop through each field that is accessible in the target type
        for (Field f : type.getFields()) {
            // get field modifiers
            final int mod = f.getModifiers();

            // check this field is not status and not final
            if (!Modifier.isStatic(mod)
                    amp;amp; !Modifier.isFinal(mod)) {
                // try to merge
                // get values of both the old and new object
                // if the new object has a null value, set the value of the new object to that of the old object
                // otherwise, keep the new value
                try {
                    final Object oldVal = f.get(oldObj);
                    final Object newVal = f.get(newObj);

                    if (newVal == null)
                        f.set(newObj, oldVal);
                } catch (IllegalAccessException e) {
                    Log.e("Tenshi", "IllegalAccess in merge: "   e.toString());
                    e.printStackTrace();
                }
            }
        }
    }
 

Ответ №1:

В room нет встроенного метода для этого

Что вы можете сделать, так это поставить галочку в запросе для вашего метода обновления.

 @Query("UPDATE person SET name = (CASE WHEN :name IS NOT NULL THEN :name ELSE name END), description = (CASE WHEN :description IS NOT NULL THEN :description ELSE description END) WHERE id = :id")
Person update(id: Int, name: String, description: String)
 

Мы написали запрос на обновление для SQL, который проверяет, являются ли вставленные значения null или нет, и если они равны null, то сохраняются предыдущие значения.

Комментарии:

1. Было бы проще / лаконичнее в использовании SET name = coalesce(:name,name) …. или ifnull(:name,name) , скорее, чем CASE WHEN .... THEN .... END

2. Привет, спасибо за этот совет, я не знал, что в SQL есть такая функция. Однако для этого потребовалось бы совсем немного кода, поэтому я решил использовать функцию, аналогичную моей первоначальной идее, но с использованием отражения.