标识触发DataIntegrityViolationException

时间:2017-02-25 23:22:17

标签: java spring hibernate jpa

我遇到了识别哪个约束触发DataIntegrityViolationException的问题。我有两个独特的限制:用户名和电子邮件,但我没有运气试图解决它。

我试图获得根本原因异常,但我收到了此消息

Unique index or primary key violation: "UK_6DOTKOTT2KJSP8VW4D0M25FB7_INDEX_4 ON PUBLIC.USERS(EMAIL) VALUES ('copeland@yahoo.com', 21)"; SQL statement: insert into users (id, created_at, updated_at, country, email, last_name, name, password, phone, sex, username) values (null, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [23505-193]

阅读错误我知道电子邮件约束会触发验证,但我想返回给用户: {type: ERROR, message: "The email already exist"}

我已阅读其他帖子,人们会在异常中处理它寻找约束名称(例如users_unique_username_idx)并向用户显示正确的消息。但我无法获得那种类型的约束名称

也许我错过了配置。我正在使用:

Spring Boot 1.5.1.RELEASE, JPA, Hibernate and H2

我的 application.properties

spring.jpa.generate-ddl=true

User.class

@Entity(name = "users")
public class User extends BaseEntity {
    private static final Logger LOGGER = LoggerFactory.getLogger(User.class);

    public enum Sex { MALE, FEMALE }

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "name", length = 100)
    @NotNull(message = "error.name.notnull")
    private String name;

    @Column(name = "lastName", length = 100)
    @NotNull(message = "error.lastName.notnull")
    private String lastName;

    @Column(name = "email", unique = true, length = 100)
    @NotNull(message = "error.email.notnull")
    private String email;

    @Column(name = "username", unique = true, length = 100)
    @NotNull(message = "error.username.notnull")
    private String username;

    @Column(name = "password", length = 100)
    @NotNull(message = "error.password.notnull")
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private String password;

    @Enumerated(EnumType.STRING)
    private Sex sex;

    @Column(name = "phone", length = 50)
    private String phone;

    @Column(name = "country", length = 100)
    @NotNull(message = "error.country.notnull")
    private String country;

    public User() {}

    // Getters and setters

}

ControllerValidationHandler.class

@ControllerAdvice
public class ControllerValidationHandler {
    private final Logger LOGGER = LoggerFactory.getLogger(ControllerValidationHandler.class);

    @Autowired
    private MessageSource msgSource;

    private static Map<String, String> constraintCodeMap = new HashMap<String, String>() {
        {
            put("users_unique_username_idx", "exception.users.duplicate_username");
            put("users_unique_email_idx", "exception.users.duplicate_email");
        }
    };

    // This solution I see in another stackoverflow answer but not work
    // for me. This is the closest solution to solve my problem that I found
    @ResponseStatus(value = HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    @ResponseBody
    public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) {
        String rootMsg = ValidationUtil.getRootCause(e).getMessage();
        LOGGER.info("rootMessage" + rootMsg);
        if (rootMsg != null) {
            Optional<Map.Entry<String, String>> entry = constraintCodeMap.entrySet().stream()
                    .filter((it) -> rootMsg.contains(it.getKey()))
                    .findAny();
            LOGGER.info("Has entries: " + entry.isPresent()); // false
            if (entry.isPresent()) {
                LOGGER.info("Value: " + entry.get().getValue());
                e=new DataIntegrityViolationException(
                        msgSource.getMessage(entry.get().getValue(), null, LocaleContextHolder.getLocale()));
            }
        }
        return new ErrorInfo(req, e);
    }

此时的回应是:

{"timestamp":1488063801557,"status":500,"error":"Internal Server Error","exception":"org.springframework.dao.DataIntegrityViolationException","message":"could not execute statement; SQL [n/a]; constraint [\"UK_6DOTKOTT2KJSP8VW4D0M25FB7_INDEX_4 ON PUBLIC.USERS(EMAIL) VALUES ('copeland@yahoo.com', 21)\"; SQL statement:\ninsert into users (id, created_at, updated_at, country, email, last_name, name, password, phone, sex, username) values (null, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [23505-193]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement","path":"/users"}

更新

这是我的服务层,用于处理我的持久性操作

MysqlService.class

@Service
@Qualifier("mysql")
class MysqlUserService implements UserService {
    private UserRepository userRepository;

    @Autowired
    public MysqlUserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public List<User> findAll() {
        return userRepository.findAll();
    }

    @Override
    public Page<User> findAll(Pageable pageable) {
        return userRepository.findAll(pageable);
    }

    @Override
    public User findOne(Long id) {
        return userRepository.findOne(id);
    }

    @Override
    public User store(User user) {
        return userRepository.save(user);
    }

    @Override
    public User update(User usr) {
        User user = this.validateUser(usr);

        return userRepository.save(user);
    }

    @Override
    public void destroy(Long id) {
        this.validateUser(id);

        userRepository.delete(id);
    }

    private User validateUser(User usr) {
        return validateUser(usr.getId());
    }

    /**
     * Validate that an user exists
     *
     * @param id of the user
     * @return an existing User
     */
    private User validateUser(Long id) {
        User user = userRepository.findOne(id);
        if (user == null) {
            throw new UserNotFoundException();
        }
        return user;
    }
}

更新#2

回购重现问题https://github.com/LTroya/boot-users。我在ValidationExceptionHandler.class上评论了我的处理程序,以便查看异常。

Json上发送两次json,在Readme.md上测试POST /users/

2 个答案:

答案 0 :(得分:2)

您要做的不是在@Column注释上指定唯一列要求,而是可以在JPA提供的@Table注释中实际定义具有名称的那些,以进一步控制这些约束

@Entity
@Table(uniqueConstraints = {
  @UniqueConstraint(name = "UC_email", columnNames = { "email" } ),
  @UniqueConstraint(name = "UC_username", columnNames = " { "userName" } )
})

现在有两种处理异常的方法:

在控制器中

您可以选择将解析逻辑放在控制器中,只需捕获春天抛出的DataIntegrityException并在那里解析它。像下面的伪代码:

public ResponseBody myFancyControllerMethod(...) {
  try {
    final User user = userService.myFactoryServiceMethod(...);
  }
  catch ( DataIntegrityException e ) {
    // handle exception parsing & setting the appropriate error here
  }
}

对我来说,这种方法的最终关键是我们已经将代码移到了两层而不是持久层上方的层来处理持久性问题。这意味着我们应该有多个需要处理这种情况的控制器,我们要么发现自己在做以下其中一种

  • 介绍一些抽象的基本控制器来放置逻辑。
  • 使用我们称之为重用的静态方法来介绍一些帮助器类。
  • 剪切粘贴代码 - 是的,这比我们想象的更多。

当您需要与可能无法实际返回某种类型的html视图的其他消费者类型共享该服务时,将代码放置在表示层中也会引入问题。

这就是为什么我建议将逻辑推低1级。

在服务中

这是一种更简洁的方法,因为我们将约束处理的验证推送到持久层上方的层,这最终是我们处理持久性失败的地方。不仅如此,我们的代码实际上记录了失败条件,我们可以根据上下文选择忽略或处理它们。

这里需要注意的是,我建议您创建从服务层代码中抛出的特定异常类,以便识别唯一约束失败,并在从Hibernate解析ConstraintViolationException之后抛出这些异常。 / p>

在您的Web控制器,休息控制器或其他任何正在调用您的服务的使用者中,您只需在必要时捕获相应的异常类并进行相应的分支。这是一些服务伪代码:

public User myFancyServiceMethod(...) {
  try {
    // do your stuff here
    return userRepository.save( user );
  }
  catch( ConstraintViolationException e ) { 
    if ( isExceptionUniqueConstraintFor( "UC_email" ) ) {
      throw new EmailAddressAlreadyExistsException();
    }
    else if ( isExceptionUniqueConstraintFor( "UC_username" ) ) {
      throw new UserNameAlreadyExistsException();
    }
  }
}

答案 1 :(得分:2)

您可以单独指定唯一约束,但您需要在实体级别上像

那样
@Entity(name = "users")
@Table(name = "users", uniqueConstraints = {
    @UniqueConstraint(name = "users_unique_username_idx", columnNames = "username"),
    @UniqueConstraint(name = "users_unique_email_idx", columnNames = "email")
})
public class User extends BaseEntity { ... }