【问题标题】:How to map temporary ids given by a frontend to generated backend ids?如何将前端给出的临时 ID 映射到生成的后端 ID?
【发布时间】:2018-11-11 07:55:41
【问题描述】:

用例:用户可以使用用 JavaScript 编写的单页 ​​Web 应用程序对多项选择题进行 CRUD。

  1. 创建一个新问题并添加一些选项都发生在浏览器/前端 (FE) 中。
  2. FE 为问题和所有选项创建并使用临时 ID(“_1”、“_2”、...),直到用户单击保存按钮。
  3. 在保存新创建的问题时,FE 将包含 临时 ID 的 JSON 发送到后端
  4. 因此,FE 期望 201 CREATED 包含一个映射 临时 id -> 后端 id 来更新其 id。
  5. 用户决定添加另一个 Option(在 FE 端再次使用临时 id)
  6. 用户点击保存,FE 发送更新后的问题,其中包含后端 ID(用于问题和现有选项)和临时 ID(用于新创建的选项)
  7. 要更新新创建的选项的 id,FE 希望响应包含此 id 的映射。

我们应该如何在后端实现最后一部分(5-7添加选项)的对应项?

我试试这个,但坚持后我无法获得孩子的ID。

实体

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

【问题讨论】:

  • 您能否描述一下您提供参数配置但您没有使用它而不是使用问题的选项构造函数,另一点是当您尝试处理新选项时,您再次使用配置null id,你能定义什么是配置以及它在你的代码中的使用方式

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


【解决方案1】:

那么,您需要将 FE-generated ID 与 BE-generated 区分开来吗? 你可以

  1. 在 FE 生成上使用负 ID,在 BE 上使用正
  2. 为 FE-generated("fe_1", "fe_2", ...) 选择特殊前缀/后缀
  3. 保留已映射 ID 的 Session 列表(服务器端)
  4. 保留 FE 生成的 ID 列表并在 POST(客户端)上将其与数据一起发送

无论如何,在混合两个 ID 生成器时要小心冲突。

【讨论】:

    【解决方案2】:

    您可以在 QuestionOption 类中创建附加字段并标记为 @Transient 以确保它不会被持久化。

    class Question {
       ....
       private String id; // actual data field
    
       @Transient
       private String tempId;
    
       // getter & setter
    }
    

    最初当 UI 发送数据时,设置tmpId 并持久化你的对象。成功操作后,id 将具有实际的 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());
    

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

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

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

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

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

    question.getOptions().clear();
    

    在此之后,您可以简单地添加新选项。

    question.setOptions(requestDTO.getOptions());
    
    question = questionRepo.save(question);
    

    我希望它现在有所帮助。

    【讨论】:

    • “在成功操作时,id 将具有实际的 id 值”当将现有问题与现有选项和新添加的选项(步骤 6 和 7)合并时,这是不正确的,因为 EM 确实创建了选项,因此瞬态字段未设置。
    • @Stuck 瞬态字段应该由 FE 设置。当他们添加新选项时,他们将拥有 tmpId(设置为“_1”或“_2”)并且在保存时,您可以执行 question.getOptions().addAll(newlyAddedOptionsList)。瞬态不会被 EM 保存。 EM 将更新 ActualId ("id"),即后端 id。
    • 它仅在所有选项都是新选项时才有效,但在合并现有选项和新选项时无效,因为合并功能的操作方式与初始保存操作不同。参见例如stackoverflow.com/a/50707702/386201
    【解决方案3】:

    最好单独保存每个选项,然后将生成的 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());//
    }
    

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

    注意不需要map来控制新的id,前端应该通过options的顺序来获取。

    【讨论】:

    • 使用命令确实可以解决问题。这是我们目前想要摆脱的解决方法,因为它是隐含的,并且对于新开发人员来说很难掌握。
    • 是的,可能很难通过订单来掌握这个选项。那么,我给出的第一个地图选项不适合您的需要?
    • 单独保存每个选项会提供 ID 并帮助映射它们,是的。在这个问题的背景下,它是一个解决方案。但是,我从我们的实际用例中简化了这个问题,这要复杂得多。在我们实际问题的上下文中,我们希望使用问题的级联来存储更多信息以防止无效的系统状态。但是您的答案是唯一真正提供解决方案的答案,我已经为您提供了赏金:) Gratz ;)
    猜你喜欢
    • 2019-10-27
    • 2019-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多