将唯一性冲突异常传播到用户界面的最佳实践

26
我们正在开发基于JPA 2、Hibernate、Spring 3和JSF 2的Java Web项目,并在Tomcat 7中运行。我们使用Oracle 11g作为数据库。
目前,我们正在就如何将数据库约束违规作为用户友好的信息显示到UI上展开讨论。更或者我们看到两种方法,但都不够令人满意。请问是否有人能给出建议? 方法1 - 编程方式验证并抛出特定异常 在CountryService.java中,每个唯一约束都将得到验证,并抛出相应的异常。这些异常会在后备bean中单独处理。 优点: 容易理解和维护。可以提供特定的用户信息。 缺点: 很多代码只是为了呈现美观的信息。基本上所有DB约束都要在应用程序中重新编写。很多查询-不必要的数据库负载。
@Service("countryService")
public class CountryServiceImpl implements CountryService {

    @Inject
    private CountryRepository countryRepository;

    @Override
    public Country saveCountry(Country country) throws NameUniqueViolationException,  IsoCodeUniqueViolationException, UrlUniqueViolationException {
        if (!isUniqueNameInDatabase(country)) {
            throw new NameUniqueViolationException();
        }
        if (!isUniqueUrl(country)) {
            throw new UrlUniqueViolationException();
        }
        if (!isUniqueIsoCodeInDatabase(country)) {
            throw new IsoCodeUniqueViolationException();
        }
        return countryRepository.save(country);
    }
}

在视图的后端Bean中处理异常:

@Component
@Scope(value = "view")
public class CountryBean {

    private Country country;

    @Inject
    private CountryService countryService;

    public void saveCountryAction() {
        try {
            countryService.saveCountry(country);
        } catch (NameUniqueViolationException e) {
            FacesContext.getCurrentInstance().addMessage("name", new FacesMessage("A country with the same name already exists."));
        } catch (IsoCodeUniqueViolationException e) {
            FacesContext.getCurrentInstance().addMessage("isocode", new FacesMessage("A country with the same isocode already exists."));
        } catch (UrlUniqueViolationException e) {
            FacesContext.getCurrentInstance().addMessage("url", new FacesMessage("A country with the same url already exists."));
        } catch (DataIntegrityViolationException e) {
             // update: in case of concurrent modfications. should not happen often
             FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("The country could not be saved."));
        }
    }
}

方法2-让数据库检测约束违规

优点:没有样板代码。没有不必要的查询到数据库。不重复数据约束逻辑。

缺点:依赖于数据库中的约束名称,因此无法通过Hibernate生成模式。需要机制将消息绑定到输入组件(例如用于突出显示)。

public class DataIntegrityViolationExceptionsAdvice {
    public void afterThrowing(DataIntegrityViolationException ex) throws DataIntegrityViolationException {

        // extract the affected database constraint name:
        String constraintName = null;
        if ((ex.getCause() != null) && (ex.getCause() instanceof ConstraintViolationException)) {
            constraintName = ((ConstraintViolationException) ex.getCause()).getConstraintName();
        }

        // create a detailed message from the constraint name if possible
        String message = ConstraintMsgKeyMappingResolver.map(constraintName);
        if (message != null) {
            throw new DetailedConstraintViolationException(message, ex);
        }
        throw ex;
    }
}

1
在第一种方法中,如果用户在唯一性检查后但在第二个用户保存之前保存了重复的国家,则仍然依赖于数据库来检测约束违规。 - ken
1
我们意识到这里存在并发问题。对于我们的使用情况来说,这并非强制性要求。如果在90%的情况下消息是具体的,那就足够了。如果在特殊情况下,由于数据库触发了更通用的消息,也无关紧要。 - fischermatte
3个回答

16

方法1在并发情况下不起作用!-- 在您检查并添加数据库记录之后,始终会有其他人插入新的数据库记录(除非您使用隔离级别可串行化,但这极不可能发生)。

因此,您必须处理DB约束违规异常。但是我建议捕获指示唯一性违规的数据库异常,并抛出更有意义的异常信息,就像您在方法1中建议的那样。


1
正如我写给Ken的那样,并发问题并不重要。在这些特殊情况下,将显示更通用的消息(由数据库触发的违规)。更新方法1。 - fischermatte
无论如何,这不会改变我的建议方法。 - Ralph
1
我不喜欢的是,在开发过程中,Hibernate通常会生成约束名称。而使用第二种方法时,我们需要通过额外的脚本单独维护这些名称。 - fischermatte
1
唯一约束具有名称属性!@javax.persistence.UniqueConstraint(name, columnName) - Ralph
3
据我所知,Hibernate会忽略UniqueConstraint上的name属性。在生成模式时,它不会被使用。 - fischermatte
仅为确认之前的声明,我在我的“用户”实体中使用了@Table(name = "users", uniqueConstraints = {@UniqueConstraint(name = "users_unique_email", columnNames = {"email"})}),然后尝试通过getConstraintName()获取具有重复电子邮件的用户。它严重依赖于底层RDBMS,因为我对不同数据库(特别是在Postgres的情况下)获得不同的约束名称 - 我得到null并且必须解析sqlException以确定哪个字段确切地被重复,如果我决定添加多个唯一约束,则可能会很麻烦。 - escudero380

15

这也可能是一个选择,而且可能成本更低,因为如果您无法直接保存,您只需要检查详细的异常:

try {
    return countryRepository.save(country);
}
catch (DataIntegrityViolationException ex) {
    if (!isUniqueNameInDatabase(country)) {
        throw new NameUniqueViolationException();
    }
    if (!isUniqueUrl(country)) {
        throw new UrlUniqueViolationException();
    }
    if (!isUniqueIsoCodeInDatabase(country)) {
        throw new IsoCodeUniqueViolationException();
    }
    throw ex;
}

3
谢谢,这比第一种方法好多了!唯一的缺点是,在调用 countryRepository.save(country) 后需要手动刷新。 - fischermatte
2
不错,但请记住,如果save在使用@Transactional注释的方法内部,则无法捕获异常。 - abarazal

1
为了避免样板代码,我在ExceptionInfoHandler中处理DataIntegrityViolationException,通过查找根本原因消息中的DB约束出现并通过映射将其转换为i18n消息。查看此处的代码:https://dev59.com/jnI95IYBdhLWcg3w3yJU#42422568

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接