validate related data when mapping view model to database model in Spring MVC
Asked Answered
Y

3

11

I'm working on a java spring mvc application and have an important question about mapping view model objects to database model objects. Our application uses dozer mapper for that purpose.

Suppose I have a Person model and BaseInformation model. The BaseInformation model is for general data that can be used in all other models, for example genders, colors, units, ....

BaseInformation:

class BaseInformation{
   private Long id;
   private String category;
   private String title;
}

This can has a database table like this:

Id | Category | Title 
-------------------------
1  | "gender" | "male"
2  | "gender" | "female"
3  | "color"  | "red"
4  | "color"  | "green"
...

This is part of my Person Model:

public class Person{
     ...
     private BaseInformation gender;
     ...
}

And this is part of my RegisterPersonViewModel

public class RegisterPersonViewModel{
    ...
    private Integer gender_id;
    ...
}

In the register person view, I have a <select> that be filled from BaseInfromation with gender category. When a user submit that form, an ajax request sends to a methods of controller like this:

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

Now, here is my question:

A user can change value of gender combobox in the view manually(for example set a value of color instead of gender) and send invalid related data to controller's method. Dozer mapper map viewModel to model and this invalid data go through data access layer and persist in database. In the other words, An invalid data can save into database without any control. I want to know the best way for controlling relational data with minimum code.

Youmans answered 2/3, 2017 at 10:43 Comment(1)
do you mean validation by check related data ?Rauch
K
6

The BaseInformation class is way too generic: gender has nothing to do with color. You need to break it up. It's a case of "One True Lookup Table" and even mentioned on Wikipedia:

In the database world, developers are sometimes tempted to bypass the RDBMS, for example by storing everything in one big table with three columns labelled entity ID, key, and value.

... which corresponds to your id, category and title.

While this entity-attribute-value model allows the developer to break out from the structure imposed by an SQL database, it loses out on all the benefits, [1] since all of the work that could be done efficiently by the RDBMS is forced onto the application instead. Queries become much more convoluted, [2] the indexes and query optimizer can no longer work effectively, and data validity constraints are not enforced.

The part in bold describes the issue you're having pretty well.


You should move the different categories into their own classes and tables. For gender an enum is good enough:

public enum Gender {
    Female, Male, Unknown, Unspecified
}

And use it in the Person class like this:

public class Person {
    ...
    private Gender gender;
    ...
}

If you're using Spring data binding to convert the input data to Java objects only the values specified in the Gender enum may be used and no further checks are necessary.

For color you could similarly use an enum if the colors don't need to be changed at runtime or a class otherwise.

Keek answered 5/3, 2017 at 21:24 Comment(2)
The baseInformation table contains more than 100 categories. So, as your suggestion, we should create more than 100 table instead of one baseInformation. This would be very difficult work for our huge project. Additionally, about 500 model have reference to baseInformation model, that might remove their reference to baseInformation and add reference to 100 alternative models. This is also a very hard work. Is there any solution for solving my problem other than splitting the baseInformation to more tables?Youmans
@Youmans I'm guessing that many of those categories don't need to be represented as tables but as new columns, like gender. Splitting up BaseInformation also doesn't have to happen all at once; you can slice off parts of it one after another, starting with gender right now. Are there other solutions? Sure, but you'd still have a giant antipattern on your hands. And since your question is only about the gender category it's not even much work to use my solution and only move gender to its own class and column.Keek
H
3

You have to validate the view model and before persisting you can also validate the entity. It seems you are using Bean Validation because you are using @Validannotation in your controller method. So just add the validation constraints for the model properties. For example:

public class RegisterPersonViewModel {
    ...
    @Min(0) @Max(1)
    private Integer gender_id;
    ...
}

But of cause if someone sends 1 and means color green and not female you are lost. Therefore it would be much better to use a enum for the gender and the other properties if possible.

Bean Validation can also be used for your database objects (entities).

Hylozoism answered 9/3, 2017 at 11:9 Comment(0)
R
1

You can use Hibernate Validator 5.x and Validation API 1.1 in Spring validation mechanism.
The new version of Hibernate Validator can validate your persistent bean as a method argument and throw javax.validation.ConstraintViolationException on your controller.

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

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

define two beans in applicationContext. Spring will automatically detect them ( another question).

<bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>

At the first glance it doesn't sound minimal code, but it is so neat and cleans the domain. It is the best solution.

Rauch answered 10/3, 2017 at 5:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.