【问题标题】:How to silently truncate strings while storing them when they are longer than the column length definition?当字符串长于列长度定义时,如何在存储字符串时静默截断字符串?
【发布时间】:2011-11-30 14:05:25
【问题描述】:

我有一个网络应用程序,使用 EclipseLink 和 MySQL 来存储数据。 其中一些数据是字符串,即数据库中的 varchars。 在实体的代码中,字符串具有如下属性:

@Column(name = "MODEL", nullable = true, length = 256)
private String model;

该数据库不是eclipseLink从代码中创建的,但长度与数据库中的varchar长度匹配。 当此类字符串数据的长度大于长度属性时,调用 javax.persistence.EntityTransaction.commit() 时会引发异常:

javax.persistence.RollbackException: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.1.0.v20100614-r7608): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'MODEL' at row 1

然后事务被回滚。 虽然我知道这是默认行为,但这不是我想要的。 我希望数据被静默截断,事务被提交。

我可以在不为相关实体的字符串数据的每个集合方法中添加对子字符串的调用的情况下执行此操作吗?

【问题讨论】:

  • 我认为答案是“不,你不能”。
  • 似乎没有人可以肯定地说出它作为答案。
  • 作为一个极端的解决方案,您可以考虑使用 AOP 框架(例如 AspectJ)来拦截对 setter 的调用。这真的很棘手,所以我不建议将其作为解决方案。
  • 如果 EclipseLink 可以配置为生成 INSERT IGNOREUPDATE IGNORE 语句,那么 MySQL 将截断这些操作。但要小心,ignore 修饰符应该只设置在真正需要和控制截断的地方。截断很危险,可能导致漏洞。

标签: java jpa eclipselink


【解决方案1】:

我对 EclipseLink 一无所知,但在 Hibernate 中它是可行的 - 您可以创建一个 org.hibernate.Interceptor 并在 onFlushDirty 方法中使用实体元数据修改对象的 currentState。

【讨论】:

    【解决方案2】:

    您可以为此使用 Descriptor PreInsert/PreUpdate 事件,或者可能只使用 JPA PreInsert 和 PreUpdate 事件。

    只需检查字段的大小并在事件代码中截断它们。

    如果需要,您可以从描述符映射的 DatabaseField 中获取字段大小,或使用 Java 反射从注解中获取。

    在你的 set 方法中进行截断可能会更好。那么你就不用担心事件了。

    您还可以在数据库中截断它,检查您的 MySQL 设置,或者可能使用触发器。

    【讨论】:

    • "在你设置的方法中进行截断。"我想这样做,但我想避免在我的代码中指定列大小,我想从数据库中读取它。我认为答案将受益于添加的细节。
    • 也许这可能有助于改进答案:stackoverflow.com/questions/673954/…
    • @Suma 如果你想从数据库中读取它,你为什么不直接使用hibernate工具来为你对数据库进行逆向工程呢?这将为您提供具有正确列长度的注释类。 AFAIK,没有办法从 Hibernate 本身获取数据库元数据(如列名、长度等)。
    【解决方案3】:

    还有另一种方法,可能更快(至少它适用于第 5 版的 MySql):

    首先,检查您的 sql_mode 设置:there is a detailed description how to do it。对于 windows,此设置的值应为 "",对于 Unix,此设置的值应为 "modes"。

    这对我没有帮助,所以我找到了另一个设置,这次是在 jdbc 中:

    jdbcCompliantTruncation=false. 
    

    就我而言(我使用了持久性),它是在 persistence.xml 中定义的:

    <property name="javax.persistence.jdbc.url" value="jdbc:mysql://127.0.0.1:3306/dbname?jdbcCompliantTruncation=false"/>
    

    这两个设置只能一起使用,我试过单独使用,没有效果。

    注意:请记住,如上所述设置 sql_mode 会关闭重要的数据库检查,因此请谨慎操作。

    【讨论】:

    • 似乎是一种危险的做事方式,因为您绕过了 ORM。假设您有二级缓存:它将包含未截断的字符串,而您的数据库将具有截断的值。我不会那样做的!
    【解决方案4】:

    您可以将数据库设置为在非严格模式下工作,如下所述: Automatically trimming length of string submitted to MySQL

    请注意,它也可能会取消其他验证,所以要小心你想要什么

    【讨论】:

      【解决方案5】:

      由于在 commit() 过程中似乎在数据库级别引发了异常,因此目标表上的插入前触发器可以在新值提交之前截断新值,从而绕过错误。

      【讨论】:

        【解决方案6】:

        也许 AOP 会有所帮助:

        拦截JavaBean/POJO中的所有设置方法,然后获取要设置的文件。检查字段是否使用@Column 进行注释,字段类型是否为String。如果它比length 太长,则截断该字段。

        【讨论】:

          【解决方案7】:

          如果您想逐个字段而不是全局地执行此操作,那么您可以进行自定义类型映射,在将值插入表之前将其截断为给定长度。然后您可以通过以下注释将转换器附加到实体:

          @Converter(name="myConverter", class="com.example.MyConverter")
          

          并通过以下方式访问相关字段:

          @Convert("myConverter")
          

          这实际上是为了支持自定义 SQL 类型,但它也可能适用于普通的 varchar 类型字段。 Here 是制作这些转换器之一的教程。

          【讨论】:

            【解决方案8】:

            UI 设计的两个非常重要的特点:

            1. 您不应默默地更改用户数据 - 用户应了解、控制并同意此类更改。
            2. 您不应允许输入无法处理的数据 - 使用 UI/HTML 属性和验证将数据限制为合法值

            您的问题的答案很简单。只需将 UI 输入字段限制为 256 个字符,以匹配数据库字段长度:

            <input type="text" name="widgetDescription" maxlength="256">
            

            这是一个系统限制 - 用户不能输入比这更多的数据。如果这还不够,请更改数据库。

            【讨论】:

            • 我原则上同意,但有时数据源不是用户。在我的应用程序中,数据来自和错误消息,并被存储以供以后调试或分析。
            • 我认为即使数据来自另一个应用程序而不是直接来自用户,物理/逻辑/概念消息或 api 合约仍然需要说明数据类型限制。验证、数据类型转换和错误处理仍然适用。如果您决定合同应允许超过 256 个字符,那么您有一个简单的解决方案 - 接受较长的字符串,然后检查其长度并根据需要截断,并将截断的字符串存储到数据库,并将完整的字符串与错误消息一起存储。在这里,您不需要在 JPA 中自动截断。
            • 显然,可以将 DB 列声明为 CLOB。来自 JPA 规范:@Column(name="DESC", columnDefinition="CLOB NOT NULL", table="EMP_DETAIL") @Lob public String getDescription() { return description; }
            【解决方案9】:

            可以根据对应字段的setter中的JPA注解截断字符串:

            public void setX(String x) {
                try {
                    int size = getClass().getDeclaredField("x").getAnnotation(Column.class).length();
                    int inLength = x.length();
                    if (inLength>size)
                    {
                        x = x.substring(0, size);
                    }
                } catch (NoSuchFieldException ex) {
                } catch (SecurityException ex) {
                }
                this.x = x;
            }
            

            注解本身应该是这样的:

            @Column(name = "x", length=100)
            private String x;
            

            (基于https://stackoverflow.com/a/1946901/16673

            如果数据库发生变化,可以从数据库中重新创建注释,如https://stackoverflow.com/a/7648243/16673的评论中所暗示的那样

            【讨论】:

            • 注意:我知道答案完全符合原始发布者排除的可能解决方案(“不向每个 set 方法添加对子字符串的调用”)。不过,我认为这种解决方案有很多好处,值得将其记录下来,以供其他人寻找解决此类问题的方法。
            【解决方案10】:

            你有不同的解决方案和错误的解决方案。

            使用触发器或任何数据库级技巧

            这将在 ORM 中的对象与其在 DB 中的序列化形式之间造成不一致。如果你使用二级缓存:它会导致很多麻烦。在我看来,这不是一般用例的真正解决方案。

            使用预插入、预更新挂钩

            您将在持久化用户数据之前静默修改用户数据。因此,您的代码的行为可能会有所不同,具体取决于对象是否已持久化。也可能带来麻烦。此外,您必须注意 hooks 调用的顺序:确保您的“field-truncator-hook”是持久性提供程序调用的第一个。

            使用 aop 拦截对 setter 的调用

            此解决方案或多或少会默默地修改用户/自动输入,但在您使用它们执行某些业务逻辑后,您的对象不会被修改。所以这比之前的两种解决方案更容易接受,但二传手不会遵循通常二传手的合同。此外,如果您使用字段注入:它将绕过方面(取决于您的配置,jpa 提供程序可能会使用字段注入。大多数情况下:Spring 使用 setter。我猜其他一些框架可能会使用字段注入,所以即使您不'不要明确使用它,请注意您正在使用的框架的底层实现)。

            使用aop拦截字段修改

            与之前的解决方案类似,只是字段注入也会被切面拦截。 (请注意,我从来没有写过一个方面做这种事情,但我认为这是可行的)

            在调用 setter 之前添加一个控制器层来检查字段长度

            可能是数据完整性方面的最佳解决方案。但它可能需要大量的重构。对于一般用例,这是(在我看来)唯一可接受的解决方案。

            根据您的用例,您可以选择其中任何一种解决方案。注意缺点。

            【讨论】:

              【解决方案11】:

              另一种选择是声明一个常量并在任何需要该长度的地方引用它,从 @Column 注释本身开始。

              然后可以将此常量转发给截断函数或验证函数,如果传递的字符串太长,则会引发预防性异常。 该常量也可以在其他一些层(例如 UI)上重复使用。

              例如:

              @Entity
              public class MyEntity {
                  public static final int NAME_LENGTH=32;
              
                  private Long id;
                  private String name;
              
                  @Id @GeneratedValue
                  public Long getId() {
                      return id;
                  }
                  protected void setId(Long id) {
                      this.id = id;
                  }
              
                  @Column(length=NAME_LENGTH)
                  public String getName() {
                      return name;
                  }
                  public void setName(String name) {
                      this.name = JpaUtil.truncate(name, NAME_LENGTH);
                  }
              }
              
              public class JpaUtil {
                  public static String truncate(String value, int length) {
                      return value != null && value.length() > length ? value.substring(0, length) : value;
                  }
              }
              

              【讨论】:

                【解决方案12】:

                已经有answer mentioned Converters,但我想添加更多细节。我的回答还假设来自 JPA 的转换器,而不是特定于 EclipseLink。

                首先创建这个类 - 特殊类型转换器,其职责是在持久化时刻截断值:

                import javax.persistence.AttributeConverter;
                import javax.persistence.Convert;
                
                @Convert
                public class TruncatedStringConverter implements AttributeConverter<String, String> {
                  private static final int LIMIT = 999;
                
                  @Override
                  public String convertToDatabaseColumn(String attribute) {
                    if (attribute == null) {
                      return null;
                    } else if (attribute.length() > LIMIT) {
                      return attribute.substring(0, LIMIT);
                    } else {
                      return attribute;
                    }
                  }
                
                  @Override
                  public String convertToEntityAttribute(String dbData) {
                    return dbData;
                  }
                }
                

                然后你可以像这样在你的实体中使用它:

                @Entity(name = "PersonTable")
                public class MyEntity {
                    
                    @Convert(converter = TruncatedStringConverter.class)
                    private String veryLongValueThatNeedToBeTruncated;
                 
                    //...
                }
                

                有关 JPA 转换器的相关文章:http://www.baeldung.com/jpa-attribute-converters

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2015-03-12
                  • 1970-01-01
                  • 1970-01-01
                  • 2015-07-08
                  • 1970-01-01
                  • 2023-02-21
                  相关资源
                  最近更新 更多