Aktualizacja tylko niektórych pól w dokumencie MongoDB

0

Piszę aplikację we frameworku Quarkus, która przechowuje dane w bazie MongoDB.

Potrzebuję dodać operację PATCH w ten sposób, aby na wejściu przekazać tylko te pola, które mają zostać zmodyfikowane.

Dane przychodzą w formacie JSON w postaci klucz-wartość, więc nie żadne RFC6902.

Prosty przykład encji - ale w rzeczywistości encje mogą mieć 100 i więcej pól różnych typów:

@Data
class Pies {
  private String imie;
  private Rasa rasa;
  private Plec plec;
  private Boolean czySzczepiony;
  private LocalDateTime dataUrodzenia;
  private String dodatkoweInformacje;
}

Możemy chcieć zaktualizować 2 pola, czyli:

{"imie":"Sonia","dodatkoweInformacje":null}

Należy dokonać walidacji danych: czy zgadzają się typy i czy nie przekazano pola o innej nazwie.

Kontroler - oba powinny zadziałać i albo dostaniemy obiekt klasy Pies (gdzie wszystkie inne pola będą null), albo mapę z polami, które przyszły.

Zakładamy, że jak przyjdzie null, to znaczy skasuj dane pole.

@Patch
@Path("/{id}")
public Pies update(@PathParam Long id, Pies patch) {
  serwis.zaktualizuj(id, patch); // nie wiemy, które pola są do aktualizacji lub usunięcia
}

@Patch
@Path("/{id}")
public Pies update(@PathParam Long id, Map<String,Object> patch) {  //lub Map<String,String>
  serwis.zaktualizuj(id, patch);
}

Jeśli zmapujemy to na obiekt klasy Pies, to tylko "imie" będzie not-null. Jeśli zignorujemy wszystkie pola null, to nie skasujemy pola "dodatkoweInformacje".

Mam kilka pomysłów:

  1. JSON skonwertować do HashMap<String,Object> a potem dokonać walidacji, czy pola w mapie istnieją w klasie i czy typy się zgadzają.
  2. JSON skonwertować do HashMap<String,String> a potem dokonać walidacji i konwersji na HashMap<String,Object>
  3. Użyć ObjectMapper, by skonwertował HashMap<String,Object> do Pies i jeśli się nie wywali, to uznajemy, że jest OK.
  4. JSON skonwerować do Pies i jednocześnie do HashMap<String,?> by odczytać pola, jakie trzeba zmodyfikować.

Każdy z tych sposobów ma pewne wady. Idealnie byłoby operować na obiekcie danej klasy zgodnie z OOP. Niestety Java nie rozróżnia w polach null od nie ustawiono.

Zacząłem implementować 2, by wymusić odpowiednie typy i spróbować dokonać konwersji danych, a może wystarczyłoby 1. Prosty wydaje się 3, ale co jeśli nie będą się zgadzać typy, np. np. "2" zamiast 2 i wrzucimy to tak do Mongo? Konwersja ObjectMapper się powiedzie, ale do bazy wrzucamy mapę, którą otrzymaliśmy na wejściu.

Do obsługi MongoDB używam Panache, który posiada metodę update().

Czy ktoś mierzył się z podobnym zagadnieniem? Jak to należy poprawnie zrobić?

A może podchodzę do tego od złej strony? Może Quarkus lub jakaś zewnętrzna biblioteka robi już coś takiego?

0
Shiba Inu napisał(a):

Jeśli zmapujemy to na obiekt klasy Pies, to tylko "imie" będzie not-null. Jeśli zignorujemy wszystkie pola null, to nie skasujemy pola "dodatkoweInformacje".

To nie jest problem Javy. Wysyłasz dane i spodziewasz się, że jakoś automagicznie serializacja stworzy obiekt, który będzie miał tylko część pól z oryginalnej klasy. To tak nie zadziała. Najszybciej byłoby napisać sobie prosty maper w Mapstruct, który będzie przyjmować dwa obiekty źródłowe i będzie decydować, której wartości użyć.

@Mapper
interface PiesMapper {

    @Mapping(target = "imie", expression = "java(merge(stary.getImie(), nowy.getImie()))")
    @Mapping(target = "czySzczepiony", expression = "java(merge(stary.isCzySzczepiony(), stary.isCzySzczepiony()))")
    Pies merge(Pies stary, Pies nowy);


   default <T> T merge(T stary, T nowy){
     return nowy !=null? nowy: stary;
   }
}

Upierdliwe? Tak. Czy działa? Tak.

0

a nie lepiej bezpośrednio zapytaniem w MongoDB?

7
Koziołek napisał(a):
@Mapping(target = "imie", expression = "java(merge(stary.getImie(), nowy.getImie()))")
@Mapping(target = "czySzczepiony", expression = "java(merge(stary.isCzySzczepiony(), stary.isCzySzczepiony()))")

(sami se przeróbcie to na twarz Jamesa Goslinga)
screenshot-20231115175738.png

0

Miałem podobny problem, napisałem własny mapper, który za pomocą refleksji próbował matchować dane. Wyglądało to mniej więcej tak:


class FieldMappers {
  static Function<String, ?> getMapper(Class<?> clazz) {
    if (clazz.equals(Integer.class)) {
      return integerMapper();
    } else if (clazz.equals(String.class)) {
      return sringMapper();
    } // tutaj jeszcze kilkanaście ifów
    else {
      throw new IllegalArgumentException();
    }
  }

  static Function<String, ?> stringMapper() {
    return s -> s;
  }

  // ...
}

class SomeEntity implements Patchable {
  private String name;
  private Integer id;

  @Override
  public void patch(Map<String, Object> map) {
    for (Map.Entry<String, Object> entry: map.entrySet()) {
      Field field = ReflectionUtils.getField(this, entry.getValue().toString());
      field.set(this, FieldMappers.getMapper(field.getType());
    }
  }
}

Oczywiście po drodze było trochę usprawnień, głównie wydajnościowych, więc czasami metodę patch rzeźbiło się ręcznie, tj.


  public void patch(Map<String, Object> map) {
    if (map.contains("name") this.name = FieldMappers.stringMapper().apply(map.get("name").toString());
    if (map.contains("id") this.id = FieldMappers.integerMapper().apply(map.get("id").toString());
    // ...
  }

Potem to ewoluowało jeszcze bardziej, w rezultacie różne klasy miały różną implementację patch.
Nie jestem specjalnie dumny - na swoje usprawiedliwienie mam dwie rzeczy:

  • byłem młody i potrzebowałem pieniędzy
  • działa do dzisiaj

Gdybym miał to zrobić dzisiaj to pewnie zrobiłbym podobnie, ewentualnie sprawdził czy jakiś Dozer lub Orika nie ogarnie tego automagicznie. Przede wszystkim jednak pilnowałbym logiki, tj. jak zasugerowano - żeby nie trzeba było podmieniać pól w obiektach trzymanych przez JVM, a zamiast tego skorzystałbym po prostu z update na bazie danych. MongoDB prawie na pewno pozwoli ci na takie selektywne poprawianie dokumentów z poziomu API.

2

Jeśli te encje mają po 100 pól, po prawdzie to po prostu chcesz ustawiać jakies pola z listy, to nie bardzo widze sens istnienia klas typu Pies (dla samego update).

Po prostu wystarczy Ci lista pól List<Field> pies = ... gdzie Field to jakaś twoja klasa opisująca jak się waliduje itd.
Tylko wtedy cały ten Panache do niczego nie jest potrzebny.

1 użytkowników online, w tym zalogowanych: 0, gości: 1