如何将前端给出的临时ID映射到生成的后端ID?

时间:2018-06-01 08:12:02

标签: spring spring-boot jpa spring-data spring-data-jpa

用例:用户可以使用以JavaScript编写的单页网页应用程序来回答多项选择题。

  1. 创建新问题并添加一些选项都发生在浏览器/前端(FE)中。
  2. 在用户点击保存按钮之前,FE会为问题和所有选项创建并使用临时ID(&#34; _1&#34;,&#34; _2&#34;,...)。< / LI>
  3. 保存新创建的问题时,FE会将包含临时ID 的JSON发送到后端
  4. 根据结果,富裕期望201 CREATED包含地图临时ID - &gt;后端ID 更新其ID。
  5. 用户决定添加另一个选项(在FE侧再次使用临时ID)
  6. 用户点击保存,FE会发送更新的问题,其中包含后端ID(针对问题和现有选项)和临时ID(针对新创建的选项)
  7. 要更新新创建的选项的ID,FE希望响应包含此ID的映射。
  8. 我们应该如何在后端实现最后一部分(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()为空

    这样的控制器应该如何创建idMap?

3 个答案:

答案 0 :(得分:1)

最好是单独保存每个选项,然后将生成的ID保存在地图上。

我在下面进行了测试,效果很好。

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

<强>更新: 或者如果前端只控制选项的顺序,并根据未保存选项的顺序获取新的ID,则将是更好的代码样式。

方法

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

控制台输出: [选项[id = 2,订单= 1],选项[id = 3,订单= 2],选项[id = 4,订单= 3],选项[id = 5,订单= 4]]

请注意,不需要地图来控制新的ID,前端应该通过选项的顺序来获取它。

答案 1 :(得分:0)

您可以在QuestionOption课程中创建其他字段,并将其标记为@Transient,以确保其不会保留。

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

   @Transient
   private String tempId;

   // getter & setter
}

最初,当UI发送数据时,请设置tmpId并保留您的对象。成功操作后,id将具有实际的id值。现在,让我们创建映射(tmpId - &gt; actualId)。

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

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

由于您只想要新创建的对象,我们可以通过两种方式实现。在创建映射时添加过滤器或稍后删除。

如上所述,在第一次保存后,UI将使用实际Id更新tmpId,在下次更新时,您将得到一个混合(实际已经保存,而tempId用于新创建)。如果已经保存,tmpId和actualId将是相同的。

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

关于您的控制器代码,您在添加新选项之前清除所有现有选项。如果您正在获取已填充了id(实际)字段的Question Object,则可以直接保留它。它不会影响任何事情。如果它有一些变化,那将是持久的。

关于您的控制器代码,正在清除

question.getOptions().clear();

在此之后,您只需添加新选项。

question.setOptions(requestDTO.getOptions());

question = questionRepo.save(question);

我希望现在有所帮助。

答案 2 :(得分:-1)

那么,您需要区分BE生成的FE生成的ID吗? 你可以

  1. 在FE生成时使用否定ID,在BE上使用正面
  2. 为FE生成选择特殊前缀/后缀(&#34; fe_1&#34;,&#34; fe_2&#34;,...)
  3. 保留已映射ID的会话列表(服务器端)
  4. 保留FE生成的ID列表,并将其与POST(客户端)
  5. 上的数据一起发送

    在任何情况下,在混合两个ID生成器时要小心碰撞。