将唯一违规异常传播到 UI 的最佳实践

Posted

技术标签:

【中文标题】将唯一违规异常传播到 UI 的最佳实践【英文标题】:Best practice propagating Unique Violation Exceptions to UI 【发布时间】:2012-04-08 05:13:55 【问题描述】:

我们正在开发基于 JPA 2、Hibernate、Spring 3 和 JSF 2 在 Tomcat 7 中运行的 Java Web 项目。我们使用 Oracle 11g 作为数据库。

我们目前正在就将违反数据库约束的方法作为用户友好的消息填充到 UI 进行辩论。我们或多或少看到了两种方式,两者都不是很令人满意。有人能给点建议吗?

方法 1 - 以编程方式验证并抛出特定异常

在 CountryService.java 中,每个 Unique 约束都将被验证并引发相应的异常。异常在支持 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);
    

在 View 的 Backing 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 - 让数据库检测违反约束的情况

优势:没有样板代码。对 db 没有不必要的查询。数据约束逻辑不重复。

缺点:依赖于 DB 中的约束名称,因此无法通过休眠生成 Schema。将消息绑定到输入组件(例如用于突出显示)所需的机制。

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 中的约束违规。 我们知道这里的并发问题。对于我们的用例,这不是强制性的。如果在 90% 的情况下消息是具体的,这就足够了。如果在特殊情况下,db 触发更通用的消息无关紧要。 【参考方案1】:

方法 1 在并发场景中不起作用! -- 在您检查之后但在您添加数据库记录之前,总是会有其他人插入新的数据库记录的变化。 (除非您使用可序列化的隔离级别,但这不太可能)

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

【讨论】:

我写信给 ken,并发问题无关紧要。在这些特殊情况下,会显示更通用的消息(将显示由 db 触发的违规行为)。更新方法 1. 无论如何,这不会改变我建议的方法。 我不喜欢它的是,通常在开发过程中,hibernate 会生成约束名称。对于方法 2,我们需要通过额外的脚本单独维护名称。 唯一约束有一个名字属性! @javax.persistence.UniqueConstraint(name, columnName) 据我所知,hibernate 会忽略 UniqueConstraint 上的 name 属性。生成架构时不会使用它。【参考方案2】:

这也可能是一种选择,而且成本可能更低,因为您只在无法完全保存时才检查详细的异常:

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;

【讨论】:

谢谢,绝对比方法 1 好!我看到的唯一缺点是,您需要在调用 countryRepository.save(country) 后手动刷新。 很好,但请记住,如果 save 位于使用 @Transactional 注释的方法中,您将无法捕获异常【参考方案3】:

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

【讨论】:

以上是关于将唯一违规异常传播到 UI 的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

自定义异常消息:最佳实践

UI 端和子项目中的 Core Data 最佳实践实现

处理 PyMySql 异常 - 最佳实践

捕获和重新抛出异常的最佳实践是啥?

Spring事务使用最佳实践

Spring事务使用最佳实践