Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to map temporary ids given by a frontend to generated backend ids?

Use Case: A user can CRUD multiple choice questions using a single page web application written in JavaScript.

  1. Creating a new question and adding some options all happens within the browser / Frontend (FE).
  2. The FE creates and uses temporary ids ("_1", "_2", ...) for both the question and all the options until the user clicks a save button.
  3. When saving the newly created question the FE sends a JSON containing the temporary ids to the backend
  4. As result the FE expects a 201 CREATED containing a map temporary id -> backend id to update its ids.
  5. The user decides to add another Option (which on the FE side uses a temporary id again)
  6. The user clicks save and the FE sends the updated question with a mixture of backend ids (for the question and the existing options) and a temporary id (for the newly created option)
  7. To update the id of the the newly created option, the FE expects the reponse to contain the mapping for this id.

How should we implement the counterpart for the last part (5-7 adding an option) on the backend side?

I try this, but I cannot get the child ids after persistence.

Entities

@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;
    }
    // ...
}

Controller

@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);
    }
}

In the controller I marked the Problem: e.getValue().getId() is null

How should such a controller create the idMap?

like image 444
Stuck Avatar asked Nov 27 '22 01:11

Stuck


1 Answers

It would be best if you save each option individually and then save the generated Id on the map.

I did the test below and it works perfectly.

@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);
}

Console out: {-1=2, -2=3, -3=4, -4=5}

UPDATED: Or will be a better code style if the front end just control de order of the options, and get the new ids based on the order of the unsaved options.

Option:

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

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

Update example:

@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());//
}

Console out: [Option [id=2, order=1], Option [id=3, order=2], Option [id=4, order=3], Option [id=5, order=4]]

Note there is no need of a map to control the new ids, the front-end should know by get it by the order of the options.

like image 58
Norberto Ritzmann Avatar answered Nov 29 '22 04:11

Norberto Ritzmann