[2.0] DB field with a unique value

1,731 views
Skip to first unread message

Drevlyanin

unread,
Apr 4, 2012, 5:04:31 AM4/4/12
to play-fr...@googlegroups.com
I tried to write a constraint on field of model, but I lacked the data context:

// CustomConstraints.java

package constraints;

import com.avaje.ebean.ExpressionList;
import play.data.validation.Constraints;
import play.db.ebean.Model;
import play.libs.F.Tuple;

import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static play.libs.F.Tuple;

public class CustomConstraints extends Constraints {
  @Target({FIELD})
  @Retention(RUNTIME)
  @Constraint(validatedBy = UniqueValidator.class)
  @play.data.Form.Display(name = "constraint.unique")
  public static @interface Unique {
    String message() default UniqueValidator.message;

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    Class<?> idClass() default Long.class;

    Class<? extends Model> modelClass();

    String[] fields();
  }

  public static class UniqueValidator extends Validator<Object> implements ConstraintValidator<Unique, Object> {
    final static public String message = "error.unique";
    private Class<? extends Model> modelClass;
    private Class<?> idClass;
    private String[] fields;

    public void initialize(Unique constraintAnnotation) {
      this.modelClass = constraintAnnotation.modelClass();
      this.idClass = constraintAnnotation.idClass();
      this.fields = constraintAnnotation.fields();
    }

    @Override
    public boolean isValid(Object o) {
      Model.Finder<?, ? extends Model> find = new Model.Finder(idClass, modelClass);
      ExpressionList el = find.where();
      for (String f : fields) {
        el.eq(f, o);
      }
      return el.findRowCount() == 0;
    }

    public Tuple<String, Object[]> getErrorMessageKey() {
      return Tuple(message, new Object[]{});
    }
  }
}

Using in model:

@Column(nullable = false, unique = true)
@Constraints.Required
@Constraints.Email
@CustomConstraints.Unique(modelClass = User.class, fields = {"email"})
public String email;

This code works correctly only if we add a new record in the database (notupdating). In the Play Framework 1.x, I used the previous values to build a correct validation. Now the constraints do not know anything about the previous values ​​of the field.

Work constrain example from Play Framework 1.x:

// Unique.java

package validation;

import net.sf.oval.configuration.annotation.Constraint;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Constraint(checkWith = UniqueCheck.class)
public @interface Unique {
  String message() default UniqueCheck.mes;
}

// UniqueCheck.java

public class UniqueCheck extends AbstractAnnotationCheck<Unique> {
  final static String mes = "validation.unique";

  @Override
  public void configure(Unique unique) {
    setMessage(unique.message());
  }

  @Override
  public boolean isSatisfied(Object validatedObject, Object value, OValContext context, Validator validator) {
    String uniqueValue = (String) value;
    if ("".equals(uniqueValue)) {
      return true;
    }
    Model object = (Model) validatedObject;
    Class cls = validatedObject.getClass();
    try {
      Method countMethod = cls.getDeclaredMethod("count", String.class, Object[].class);
      String fieldName = ((FieldContext) context).getField().getName();
      Long count;
      if (object.id == null) { // yet not saved
        count = (Long) countMethod.invoke(null, fieldName + " = ?1", new Object[]{uniqueValue});
      } else {
        count = (Long) countMethod.invoke(null, fieldName + " = ?1 and id != ?2", new Object[]{uniqueValue, object.id});
      }
      return count == 0;
    } catch (Exception e) {
      return false;
    }
  }
}

Using in model:

@Email
@Unique
@Required
@Column(nullable = false, unique = true)
public String email;

There is a thought, how to make a unique constraint in the Play Framework 2.x?

Leonard Punt

unread,
Apr 4, 2012, 6:57:47 AM4/4/12
to play-fr...@googlegroups.com
I've started to write a Unique constraint few weeks ago, but haven't had time to finish it.

My idea is:
1. Create a class-constraint instead of a field-constraint, so @Target({FIELD}) becomes @Target({TYPE})
2. The constraint has 2 parameters: the name of the 'id' field (default 'id') and the name of the unique field
3. The parameter of the isValid(Object object) method contains the model object
4. With help of BeanUtils.getBeanProperty(object, uniqueFieldName); and BeanUtils.getBeanProperty(object, idFieldName); you can get the current values of your id and uniqueField

And then you should have al the required information to check whether your uniqueField contains a unique value or not.

If you manage to create a Unique constraint, I'm interested in your solution!


Op woensdag 4 april 2012 11:04:31 UTC+2 schreef Drevlyanin het volgende:

GrailsDeveloper

unread,
Apr 4, 2012, 9:58:37 AM4/4/12
to play-fr...@googlegroups.com
Why do you want to put the constraint to the class? I think the field is the right place. Specially you can have the situation that you want to have more than one unique constraint.
Niels

GrailsDeveloper

unread,
Apr 4, 2012, 10:04:56 AM4/4/12
to play-fr...@googlegroups.com
I think you must check if is an update or an insert. I did it in the play1.0-code by looking if the key-value is null. In play 1.0 it was easy to get this information, but I think is shouldn't be a problem at Ebean, because there is an @Id annotation. So I think you are on the right way, just add this handling.
And if you finish, please wrote how to write checks for play 2.0
Niels

Leonard Punt

unread,
Apr 4, 2012, 10:06:28 AM4/4/12
to play-fr...@googlegroups.com
Because there is no other way to get the value of the id, at least not that I know of.

In case you want more than one unique constraint, just replace the unique parameter to a List of unique parameters.

Other solution would be using a framework, like http://oval.sourceforge.net/. This framework is also used for the Play1.2.4 Unique annotation.

Op woensdag 4 april 2012 15:58:37 UTC+2 schreef GrailsDeveloper het volgende:

Drevlyanin

unread,
Apr 4, 2012, 10:45:48 AM4/4/12
to play-fr...@googlegroups.com
1. Create a class-constraint instead of a field-constraint, so @Target({FIELD}) becomes @Target({TYPE}) 
3. The parameter of the isValid(Object object) method contains the model object 

It's good idea!

Ben McCann

unread,
Apr 4, 2012, 1:39:17 PM4/4/12
to play-fr...@googlegroups.com
I normally just annotate my field with the JPA annotation:
    @Column(unique=true)

Id there any reason this doesn't work for you?

Leonard Punt

unread,
Apr 4, 2012, 2:07:14 PM4/4/12
to play-fr...@googlegroups.com
The annotation @Column(unique=true) won't give a validation error if you try to validate an object.

Op woensdag 4 april 2012 19:39:17 UTC+2 schreef Ben McCann het volgende:

Ben McCann

unread,
Apr 4, 2012, 2:46:42 PM4/4/12
to play-fr...@googlegroups.com
It would be a poor practice to validate the object and then try to save it as a separate step.  Firstly, this means you need to hit the database twice.  Secondly, the validation could pass, someone else could write to the database, and then you try to write with the same value and you still fail anyway.


--
You received this message because you are subscribed to the Google Groups "play-framework" group.
To view this discussion on the web visit https://groups.google.com/d/msg/play-framework/-/5xzdSF5_mMoJ.

To post to this group, send email to play-fr...@googlegroups.com.
To unsubscribe from this group, send email to play-framewor...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/play-framework?hl=en.

Leonard Punt

unread,
Apr 4, 2012, 3:08:41 PM4/4/12
to play-fr...@googlegroups.com
I agree with your points. But if you want to check if someone's, lets say, username is unique you have to talk to the database. I think it is something you want to check before trying to save the username. Or you have to catch some persistence exception. That's not a very nice practice either, is it?

Ben McCann

unread,
Apr 4, 2012, 3:23:16 PM4/4/12
to play-fr...@googlegroups.com
Good point.  I don't usually use a Validator for that because I'm only checking one field since the rest of the form is not filled out (unless that's something validator can handle?)  I've just been doing it manually, so I'm afraid I don't have any good advice.


On Wed, Apr 4, 2012 at 12:08 PM, Leonard Punt <leona...@gmail.com> wrote:
I agree with your points. But if you want to check if someone's, lets say, username is unique you have to talk to the database. I think it is something you want to check before trying to save the username. Or you have to catch some persistence exception. That's not a very nice practice either, is it?

--
You received this message because you are subscribed to the Google Groups "play-framework" group.
To view this discussion on the web visit https://groups.google.com/d/msg/play-framework/-/e9Ml4cUnFCQJ.

Drevlyanin

unread,
Apr 5, 2012, 8:32:19 AM4/5/12
to play-fr...@googlegroups.com
Firstly, this means you need to hit the database twice.

For single checks (for one record) is not critical.

Secondly, the validation could pass, someone else could write to the database, and then you try to write with the same value and you still fail anyway.
 
What actions need to be wrapped in a transaction (@play.db.ebean.Transactional).
Message has been deleted

Drevlyanin

unread,
Apr 5, 2012, 8:34:58 AM4/5/12
to play-fr...@googlegroups.com
By the way, the implementation of such restrictions can be taken to a separatemodule.

Dmitriy Arkhipov

unread,
Apr 6, 2012, 9:24:30 AM4/6/12
to play-fr...@googlegroups.com
I updated constrain:

// User.java

@Entity
@Table(name = "users")
@CustomConstraints.Unique(id = "id", fields = {"email"})
public class User extends Model {


// CustomConstraints.java
package constraints;

import com.avaje.ebean.ExpressionList;
import org.apache.commons.beanutils.PropertyUtils;
import play.data.validation.Constraints;
import play.db.ebean.Model;
import play.libs.F.Tuple;

import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static play.libs.F.Tuple;

public class CustomConstraints extends Constraints {
  @Target({TYPE})
  @Retention(RUNTIME)
  @Constraint(validatedBy = UniqueValidator.class)
  @play.data.Form.Display(name = "constraint.unique")
  public static @interface Unique {
    String message() default UniqueValidator.message;

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String id();

    String[] fields();
  }

  public static class UniqueValidator extends Validator<Object> implements ConstraintValidator<Unique, Object> {
    final static public String message = "error.unique";
    private String id;
    private String[] fields;

    public void initialize(Unique constraintAnnotation) {
      id = constraintAnnotation.id();
      fields = constraintAnnotation.fields();
    }

    @Override
    public boolean isValid(Object o) {
      try {
        Model.Finder<?, ? extends Model> find = new Model.Finder(id.getClass(), o.getClass());

        ExpressionList el = find.where();
        Object idValue = PropertyUtils.getProperty(o, id);
        if (idValue != null) {
          el.ne(id, idValue);
        }
        for (String f : fields) {
          el.eq(f, PropertyUtils.getProperty(o, f));
        }
        return el.findRowCount() == 0;
      } catch (Exception e) {
        return false;
      }
    }

    public Tuple<String, Object[]> getErrorMessageKey() {
      return Tuple(message, new Object[]{});
    }
  }
}

// UserTest.java
User u = new User();
u.email = "us...@email.com";
u.save();

User u2 = new User();
u2.email = "us...@email.com";
u2.save();
assertThat(validator.validate(u2).size()).isEqualTo(1); // pass
assertThat(validator.validateProperty(u2, "email").isEmpty()).isFalse(); //fail

But in this case validateProperty don't work correct. :(

Can we get from "Object o" or "javax.validation.ConstraintValidatorContext constraintValidatorContext" User object? 

Leonard Punt

unread,
Apr 11, 2012, 5:48:53 PM4/11/12
to play-fr...@googlegroups.com
Sorry for the late response!

But 'Object o' is the User object, isn't it?

Dmitriy Arkhipov

unread,
Apr 12, 2012, 4:22:08 AM4/12/12
to play-fr...@googlegroups.com
Yes, "Object o" is User object. If we use "@Target({TYPE})" annotation for constraint, then

assertThat(validator.validateProperty(u2, "email").isEmpty()).isFalse(); //fail

don't work in this case.

If we use "@Target({FIELD})" annotation for constraint then "Object o" isn't User object.

Leonard Punt

unread,
Apr 12, 2012, 4:29:19 AM4/12/12
to play-fr...@googlegroups.com
Okay, I see. As far as I know it is not possible to get the User object if you are using @Target({ FIELD }). I recommend to look at a library like this one: http://oval.sourceforge.net/. Play1.0 uses this library too for their UniqueCheck: https://github.com/playframework/play/blob/master/framework/src/play/data/validation/UniqueCheck.java.

Op donderdag 12 april 2012 10:22:08 UTC+2 schreef Dmitriy Arkhipov het volgende:

GrailsDeveloper

unread,
Apr 12, 2012, 5:58:55 AM4/12/12
to play-fr...@googlegroups.com
I don't think that it is a good idea to mix validation frameworks. Still wondering that it doesn't work with the new framework, but don't find the time to look deeper into it.
Niels

Dmitriy Arkhipov

unread,
Apr 27, 2012, 10:51:56 AM4/27/12
to play-fr...@googlegroups.com
https://github.com/playframework/play/blob/master/framework/src/play/data/validation/UniqueCheck.java is tasty. This would be for the 2.x version. It is possible for the 2.x version?

GrailsDeveloper

unread,
Apr 27, 2012, 11:17:48 AM4/27/12
to play-fr...@googlegroups.com
I wrote the  UniqueCheck.java , how ever I think it's not so easy to migrate, because it's based on the oval-framework. The new framework is more standard, but obvious not so good :-( I hoped that you can solve it.I found  http://stackoverflow.com/questions/4613055/hibernate-unique-key-validation and it looks like it can only be solved be a type check. Then you don't have a property specific message. Have you search for possibilities to write directly validation-errors? In play 1.x I do something like this:
public void setPassword(String newPassword) {
        if (newPassword != null && newPassword.equals(this.password)) {
            //Prevent that the hash will be hashed again.
            return;
        }
        if (newPassword  != null && newPassword.length() > 0) {
            if (newPassword.length() >= 6) {
                this.password = PasswordTools.saltPasswordBase64(newPassword);
            } else {
                Validation.current().addError("password", "validation.minSize", "6");
            }
        } else {
            Validation.current().addError("password", "validation.required");
        }
    }

but I'm not familiar enough with play 2, to check if this could be a way.

Niels

GrailsDeveloper

unread,
Apr 27, 2012, 4:02:09 PM4/27/12
to play-fr...@googlegroups.com
Have you follow this thread https://groups.google.com/d/topic/play-framework/1-JM9yHfy5Y/discussion. Perhaps you can put the check into the validate-method? You can write a helper class with analyse your model and create a Map with validation-errors.
Niels
Reply all
Reply to author
Forward
0 new messages