我正在使用java spring mvc应用程序并且有一个关于将视图模型对象映射到数据库模型对象的重要问题。我们的应用程序使用dozer mapper来实现此目的。
假设我有 Person 模型和 BaseInformation 模型。 BaseInformation模型适用于可在所有其他模型中使用的一般数据,例如性别,颜色,单位...... 。
BaseInformation:
class BaseInformation{
private Long id;
private String category;
private String title;
}
这可以有这样的数据库表:
Id | Category | Title
-------------------------
1 | "gender" | "male"
2 | "gender" | "female"
3 | "color" | "red"
4 | "color" | "green"
...
这是人员模型的一部分:
public class Person{
...
private BaseInformation gender;
...
}
这是我的 RegisterPersonViewModel
的一部分public class RegisterPersonViewModel{
...
private Integer gender_id;
...
}
在注册人视图中,我有一个<select>
,可以从 BaseInfromation 填充性别类别。当用户提交该表单时,ajax请求会发送到控制器的方法,如下所示:
@RequestMapping("/person/save", method = RequestMethod.POST, produces = "application/json")
public @ResponseBody Person create(@Valid @RequestBody RegisterPersonViewModel viewModel) throws Exception {
//Mapping viewModel to Model via dozer mapper
//and passing generated model to service layer
}
现在,我的问题是:
用户可以手动更改视图中的value
性别组合框(例如设置颜色而不是性别),并将无效的相关数据发送到控制器的方法。 Dozer映射器将viewModel映射到模型,这个无效数据通过数据访问层并在数据库中保留。换句话说,无效数据可以在没有任何控制的情况下保存到数据库中。我想知道使用最少代码控制关系数据的最佳方法。
答案 0 :(得分:6)
BaseInformation类过于通用:性别与颜色无关。你需要分手。这是“One True Lookup Table”的案例,甚至在Wikipedia上提到:
在数据库领域,开发人员有时会试图绕过RDBMS,例如将所有内容存储在一个包含三个标记为实体ID,密钥和值的列的大表中。
...对应于您的ID,类别和标题。
虽然这个实体 - 属性 - 值模型允许开发人员从SQL数据库强加的结构中脱离出来,但它会失去所有的好处[1],因为所有的工作都可以通过RDBMS被强制转换为应用程序。查询变得更加复杂,[2]索引和查询优化器无法再有效工作,数据有效性约束不会强制执行。
粗体部分描述了你很好的问题。
您应该将不同的类别移动到他们自己的类和表中。对于性别而言,枚举就足够了:
public enum Gender {
Female, Male, Unknown, Unspecified
}
并在Person
类中使用它,如下所示:
public class Person {
...
private Gender gender;
...
}
如果您使用Spring数据绑定将输入数据转换为Java对象,则只能使用Gender
枚举中指定的值,不需要进一步检查。
对于颜色,如果颜色不需要在运行时或其他类别更改,则可以类似地使用枚举。
答案 1 :(得分:3)
您必须验证视图模型,在持久化之前,您还可以验证实体。
您似乎正在使用Bean验证,因为您在控制器方法中使用@Valid
注释。因此,只需为模型属性添加验证约束。例如:
public class RegisterPersonViewModel {
...
@Min(0) @Max(1)
private Integer gender_id;
...
}
但是,如果有人发送1并且意味着颜色为绿色而不是女性则会丢失。因此,如果可能的话,对性别和其他属性使用枚举会更好。
Bean Validation也可用于您的数据库对象(实体)。
答案 2 :(得分:1)
您可以在Hibernate Validator 5.x中使用Validation API 1.1和Spring validation mechanism
新版本的Hibernate Validator可以将持久化bean作为方法参数进行验证,并在控制器上抛出javax.validation.ConstraintViolationException
。
@Inject MyPersonService myPersonService;
@RequestMapping("/person/save", method = RequestMethod.POST, produces = "application/json")
public @ResponseBody Person create( @RequestBody RegisterPersonViewModel viewModel) throws Exception {
Person person = ...; // map
try{
myPersonService.persist(person);
}catch (ConstraintViolationException cvex) {
for (ConstraintViolation cv : cvex.getConstraintViolations()) {
String errorMessage = cv.getMessage();
}
}
}
服务:
@Service
public class MyPsersonService{
public void persist(@Valid Person person){
// do persist here without checking related data
}
}
pserson:
public class Person{
...
@ValidCategory("gender")
private BaseInformation gender;
...
}
ValidCategory:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
@Constraint(validatedBy = CategoryValidator.class)
public @interface ValidCategory {
String message() default "{info.invalid}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
String value(); // the category name goes here
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Documented
@interface List {
ValidCategory[] value();
}
}
CategoryValidator:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class CategoryValidator implements ConstraintValidator<ValidCategory, BaseInformation> {
private String category;
@Override
public void initialize(ValidCategory validCat) {
this.category = validCat.value();
}
@Override
public boolean isValid(BaseInformation baseInfo, ConstraintValidatorContext cvc) {
if(!this.category.equals(baseInfo.getCategory()){
addError(cvc,"you've entered invalid category!");
return false;
}else{
return true;
}
}
private void addError(ConstraintValidatorContext cvc, String m){
cvc.buildConstraintViolationWithTemplate(m).addConstraintViolation();
}
}
在applicationContext中定义两个bean。 Spring会自动检测它们(another question)。
<bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>
乍一看它听起来不是最小的代码,但它是如此整洁并清理域名。这是最好的解决方案。