Closed eloquenza closed 6 years ago
So, wie der Wunsch von Julius war - eine Annotation fuer einen Class Member, um in der Datenbank nach seiner Einzigartigkeit zu pruefen. Das ist quasi schwarze Magie.
Genutzt wird es wie folgt:
public class CreateUserDto {
@Constraints.Required
@Constraints.MaxLength(MAX_USERNAME_LENGTH)
@HsHConstraints.Unique(model = User.class, columns = "username", message = "Dieser Username existiert bereits")
private String username;
//...
}
Der dahinter liegende Code sieht wie folgt aus:
public class HsHConstraints {
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = UniqueValidator.class)
@Repeatable(Unique.List.class)
public @interface Unique {
String message() default UniqueValidator.message;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Model> model();
String[] columns();
/**
* Defines several {@code @Unique} annotations on the same element.
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface List {
Unique[] value();
}
}
public static class UniqueValidator extends play.data.validation.Constraints.Validator<String> implements ConstraintValidator<Unique, String> {
final static public String message = "error.unique";
private String[] columns;
private Class<? extends Model> model;
public void initialize(Unique constraintAnnotation) {
this.model = constraintAnnotation.model();
columns = constraintAnnotation.columns();
}
public boolean isValid(String object) {
Finder<Long, ? extends BaseDomain> find = new Finder(model);
ExpressionList el = find.query().where();
for (String f: columns) {
el.eq(f, object);
}
return el.findCount() == 0;
}
public Tuple<String, Object[]> getErrorMessageKey() {
return Tuple(message, new Object[]{});
}
}
}
Ist das haesslich? Jap. Funktioniert's? Leider sehr. Und man kanns fuers jede Class Member benutzen, welches unique sein soll. Nachteile?
Warum kann man bei einem ConstraintValidator
nichts injecten kann, siehe hier JSR380Docu:
The default ConstraintValidatorFactory provided by Hibernate Validator requires a public no-arg constructor to instantiate ConstraintValidator instances (see Section 6.1.2, “The constraint validator”). Using a custom ConstraintValidatorFactory offers for example the possibility to use dependency injection in constraint validator implementations.
Da diese Constraints von Play alle auf dem JSR basiert sind, weil das die Standardimplementation dieser ganzen verrueckten Beangeschichte ist, werden also ConstraintValidator default constructed.
Wir koennten also versuchen, via Guice irgendwie in unsere Application einen ConstraintValidatorFactory zu injecten, der alle Finder bekommt, damit jeder Constraint automatisch die Finder enthaelt. :) :) :) :) :)
Siehe commit: 90b2a1ab6d27ccde83bc5c4070e21e2352e42de5 vom branch grabowski/implement-a-field-annotation-for-uniqueness
Die komplett generische Version find ich deutlich zu komplex für unseren Anwendungsfall. Finde diesen Weg hier am besten. #42
Ok, dann trenn ich das eben auf in 2 Klassen, die absolut hardkodiert sind und genau das selbe machen, bis auf 2 unterschiedliche Werte. Und dann spaeter wohl nochmal fuer Files. Weiss nicht, wie ich das um ehrlich zu sein mit guten Gewissen vertreten kann, noch weniger, wie ich das in einer Architekturbeschreibung, die Entscheidung fuer dieses Design als Vorteil aufzeigen kann.
Julius hatte heute, richtigerweise, angemerkt, dass die Mechanik hinter dem momentanen, von mir aus der Dokumentation abprogrammierten Validationsprozess nicht elegant ist, da ein Cast noetig ist, um auf den richtigen Typ zu kommen und auf dessen Methoden zugreifen zu koennen; hier die Queries des UserFinders.
https://github.com/Geend/HshHelper/blob/2536987285bdf311a8e1554c8cda50aa739db4f1/src/app/models/dtos/CreateUserDto.java#L54-L57
Ich hab nachgedacht, und bin zu dem Entschluss gekommen, dass ich nicht weiss, wie das schoener geht, wenn wir Subtyping von dem selben Interface (
ValidatableWithFinder
) wuenschen, damit wir eine Annotation@ValidateWithGroupFinder
und einen@ValidateWithUserFinder
haben, ohne fuer jedes noch ein eigenes Interface zu schreiben, weil das daemlich erscheint.Das Problem basiert darauf, dass dank des Liskovischen Substitutionsprinzip kein kovarianter Methodentyp erlaubt ist, da sonst keine Garantie bezueglich semantischer Interoperabilitaet von Typen innerhalb einer Objekthierarchie gegeben werden kann. Java folgt diesem Prinzip natuerlich, was nur richtig ist.
Um das auszuformulieren, damit man das verstehen kann:
https://github.com/Geend/HshHelper/blob/2536987285bdf311a8e1554c8cda50aa739db4f1/src/app/validation/ValidatableWithFinder.java#L5-L7
Der Validator nutzt dann das ganze wie folgt - ja, ich weiss, dass die Namen scheisse sind. Ich habe mich an die Nomenklatur von Play gehalten:
https://github.com/Geend/HshHelper/blob/2536987285bdf311a8e1554c8cda50aa739db4f1/src/app/validation/ValidateWithUserFinderValidator.java#L10-L17
Da UserFinder und GroupFinder nur Spezialisationen von
Finder<Long, Group | User>
sind, koennen sie nicht anstelle vonFinder<Long, Model>
genutzt werden, da sie eine andere Garantie bezueglich ihres Verhaltens bieten. Beide Subtypen bieten eine speziellere Logik, daher kann Java nicht garantieren, dass sie sich im Bezug auf ihren Supertyp aehnlich verhalten.Durch diese Loesung von mir gewinnen wir allerdings Typsicherheit - der Finder muss den richtigen Typ haben, der vom Validator angegeben wird. Nutzt man ausversehen
Finder<Long, Group>
imCreateUserDto
, trifft man ansonsten auf folgenden Fehler:validate(Finder<Long, Group>)' in 'models.dtos.CreateUserDto' clashes with 'validate(Finder<Long, Model>)' in 'validation.ValidatableWithFinder'; both methods have same erasure, yet neither overrides the other
Ob das nun nen Vorteil ist, weiss ich nicht. Schaetze nicht.
Man koennte auch sagen, Java ist scheisse, weil die Generics Type Erasure nutzen. :P
Die einzige Loesung, die ich sehe, ist entweder eine Abaenderung und Entkopplung durch Entwicklung zweier verschiedener Interfaces (die dann mehr werden, sobald wir ggf. zum Dateiaustausch kommen, da wir evtl noch so nen Validator brauchen). Oder nochmal schauen, wie das funktioniert, nur ein Field eines DTOs/Models zu validieren. Vllt ist das die schoenere Variante.
Habt ihr ne bessere Idee?