Как сопоставить временные идентификаторы, данные внешним интерфейсом, сгенерированным внутренним идентификаторам? - PullRequest
0 голосов
/ 01 июня 2018

Вариант использования : пользователь может задавать вопросы с несколькими вариантами ответов с помощью одностраничного веб-приложения, написанного на JavaScript.

  1. Создание нового вопроса и добавление некоторых параметров происходит в пределахbrowser / Frontend (FE).
  2. FE создает и использует временные идентификаторы ("_1", "_2", ...) для вопроса и всех опций, пока пользователь не нажмет кнопку сохранения.
  3. При сохранении вновь созданного вопроса FE отправляет JSON, содержащий временные идентификаторы , на серверную часть
  4. В результате FE ожидает 201 CREATED, содержащий карту временный идентификатор -> внутренний идентификатор для обновления своих идентификаторов.
  5. Пользователь решает добавить еще одну опцию (которая на стороне FE снова использует временный идентификатор)
  6. Пользователь нажимает сохранить иFE отправляет обновленный вопрос со смесью внутренних идентификаторов (для вопроса и существующих опций) и временного идентификатора (для вновь созданной опции)
  7. Чтобы обновить идентификатор вновь созданной опции,FE ожидает, что ответ будет содержать сопоставление для этого идентификатора.

Как мы должны реализовать аналог для последней части (5-7 добавление опции) на стороне сервера?

Я пытаюсь это сделать, но не могу получить дочерние идентификаторы после сохранения.

Сущности

@Entity
public class Question {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @OneToMany(mappedBy = "config", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Option> options = new ArrayList<>();
    // ...
}


@Entity
public class Option {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "question_id", nullable = false)
    private Question question;

    public Option(Long id, Config config) {
        this.id = id;
        this.question = question;
    }
    // ...
}

Контроллер

@RestController
@RequestMapping("/questions")
public class AdminQuestionsController {

    @Autowired
    private QuestionRepository questionRepo;

    @Autowired
    private OptionRepository optionRepo;

    @PutMapping("/{id}")
    @ResponseStatus(HttpStatus.OK)
    public QuestionDTO updateQuestion(@PathVariable("id") String id, @RequestBody QuestionDTO requestDTO) {
        Question question = questionRepo.findOneById(Long.parseLong(id));

        // will hold a mapping of the temporary id to the newly created Options.
        Map<String, Option> newOptions = new HashMap<>();

        // update the options        
        question.getOptions().clear();

        requestDTO.getOptions().stream()
            .map(o -> {
                try { // to find the existing option
                    Option theOption = question.getOptions().stream()
                            // try to find in given config
                            .filter(existing -> o.getId().equals(existing.getId()))
                            .findAny()
                            // fallback to db
                            .orElse(optionRepo.findOne(Long.parseLong(o.getId())));
                    if (null != theOption) {
                        return theOption;
                    }
                } catch (Exception e) {
                }
                // handle as new one by creating a new one with id=null
                Option newOption = new Option(null, config);
                newOptions.put(o.getId(), newOption);
                return newOption;
            })
            .forEach(o -> question.getOptions().add(o));

        question = questionRepo.save(question);

        // create the id mapping
        Map<String, String> idMap = new HashMap<>();
        for (Entry<String, Option> e : newOptions.entrySet()) {
            idMap.put(e.getKey(), e.getValue().getId());
            // PROBLEM: e.getValue().getId() is null 
        }

        return QuestionDTO result = QuestionDTO.from(question, idMap);
    }
}

В контроллере я отметил проблему: e.getValue (). GetId () имеет значение null

Как такой контроллер должен создать idMap?

Ответы [ 3 ]

0 голосов
/ 08 июня 2018

Было бы лучше, если бы вы сохранили каждую опцию индивидуально, а затем сохранили сгенерированный идентификатор на карте.

Я выполнил приведенный ниже тест, и он отлично работает.

@Autowired
void printServiceInstance(QuestionRepository questions, OptionRepository options) {
    Question question = new Question();

    questions.save(question);

    question.add(new Option(-1L, question));
    question.add(new Option(-2L, question));
    question.add(new Option(-3L, question));
    question.add(new Option(-4L, question));

    Map<Long, Long> idMap = new HashMap<>();

    question.getOptions().stream()
            .filter(option -> option.getId() < 0)
            .forEach(option -> idMap.put(option.getId(), options.save(option).getId()));

    System.out.println(idMap);
}

Выход на консоль: {-1 = 2, -2 = 3, -3 = 4, -4 = 5}

ОБНОВЛЕНО: Или будет лучше код, есливнешний интерфейс просто контролирует порядок опций и получает новые идентификаторы на основе порядка несохраненных опций.

Опция:

@Column(name = "order_num")
private Integer order;

public Option(Long id, Integer order, Question question) {
    this.id = id;
    this.question = question;
    this.order = order;
}

Пример обновления:

@Autowired
void printServiceInstance(QuestionRepository questions, OptionRepository options) {
    Question question = new Question();

    Question merged = questions.save(question);

    merged.add(new Option(-1L, 1, merged));
    merged.add(new Option(-2L, 2, merged));
    merged.add(new Option(-3L, 3, merged));
    merged.add(new Option(-4L, 4, merged));

    questions.save(merged);

    System.out.println(questions.findById(merged.getId()).get().getOptions());//
}

Выход консоли: [Option [id = 2, order = 1], Option [id = 3, order = 2], Option [id = 4, order =3], Option [id = 5, order = 4]]

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

0 голосов
/ 09 июня 2018

Итак, вам нужно отличить сгенерированный FE идентификатор от сгенерированного BE?Вы можете

  1. использовать отрицательный идентификатор для сгенерированного FE, положительный для BE
  2. выбрать специальный префикс / суффикс для сгенерированного FE ("fe_1", "fe_2", ...)
  3. сохранить в списке сеансов уже назначенные идентификаторы (на стороне сервера)
  4. сохранить список сгенерированных FE идентификаторов и отправить его с данными на POST (на стороне клиента)

В любом случае, остерегайтесь столкновений при смешивании двух генераторов ID.

0 голосов
/ 05 июня 2018

вы можете создать дополнительное поле в классах Question и Option и пометить как @Transient, чтобы убедиться, что оно не сохраняется.

class Question {
   ....
   private String id; // actual data field

   @Transient
   private String tempId;

   // getter & setter
}

Изначально, когда пользовательский интерфейс отправляет данные, установите tmpIdи сохраняй свой объект.При успешной операции id будет иметь фактическое значение идентификатора.Теперь давайте создадим отображение (tmpId -> actualId).

Map<String, String> mapping = question.getOptions().stream()
    .collect(Collectors.toMap(Option::getTmpId, Option::getId, (first, second) -> second));

mapping.put(question.getTmpId(), question.getId());

Поскольку вам нужен только недавно созданный объект, мы можем сделать это двумя способами.Либо добавьте фильтр при создании сопоставления, либо удалите позже.

Как уже упоминалось, после первого сохранения пользовательский интерфейс обновит tmpId с использованием фактического идентификатора, а при следующем обновлении вы получите микс (актуальный для уже сохраненного и временный идентификатор для нового).создано).В случае уже сохраненных, tmpId и actualId будут одинаковыми.

mapping.entrySet().removeIf(entry -> entry.getKey().equals(entry.getValue()));

Что касается кода вашего контроллера, вы очищаете все существующие опции перед добавлением новой опции.Если вы получаете Объект Вопроса с уже заполненным полем id (фактическим), вы можете сохранить его напрямую.это ни на что не повлияет.Дополнительно, если в нем есть какие-то изменения, это будет сохраняться.

Относительно кода вашего контроллера, когда вы очищаете

question.getOptions().clear();

После этого вы можете просто добавить новые опции.

question.setOptions(requestDTO.getOptions());

question = questionRepo.save(question);

Надеюсь, теперь это поможет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...