返回

Grails 5 中解决 MySQL 索引长度限制及兼容性问题

mysql

Grails 5 中如何为 MySQL 域属性定义索引长度

碰到了一个棘手的问题:想在 Grails 5 的域类中给一个 String 类型的属性设置索引,同时还得兼容不同版本的 MySQL,特别是处理索引长度限制这块,有点麻烦。

问题

目前我的 Grails 域类里是这样写的:

String value
static constraints = {
    value(blank: false, size: 1..400)
}
static mapping = {
    value index: 'value_Idx'
}

数据库用的是 MySQL InnoDB,它的索引长度最大是 767 字节。如果用 UTF-8 编码,最长只能索引 255 个字符,所以需要用到前缀索引。

在 MySQL 5.5 或更低版本里,Grails 不会报错,查看生成的索引,发现前缀长度被默默地调整成了 255:

mysql> show index from table;
+--------+-------------+--------+----------+--------+
|  ....  | Column_name |  ....  | Sub_part |  ....  |
+--------+-------------+--------+----------+--------+
|  ....  | value       |  ....  | 255      |  ..... |
+--------+-------------+--------+----------+--------+

但在 MySQL 5.6 里,启动 Grails 应用时就会报错:

ERROR hbm2ddl.SchemaUpdate - Unsuccessful: create index value_Idx on table (value)
ERROR hbm2ddl.SchemaUpdate - Index column size too large. The maximum column size is 767 bytes.

查了文档才知道,在老版本的 MySQL,会悄悄地降低最大索引长度.但是MySQL5.6之后,会抛出一个错误。

为了兼容所有 MySQL 版本,我想在域类的映射里显式地指定前缀索引的长度 。 这能实现吗?具体咋搞?

我在 MySQL 控制台里知道怎么弄,但不知道在 Grails/GORM/Hibernate 里怎么弄。

问题原因

问题的根源在于 MySQL InnoDB 引擎对索引长度的限制,以及不同版本 MySQL 对超出限制的处理方式不同:

  • MySQL InnoDB 的索引长度限制为 767 字节。
  • 使用 UTF-8 编码时,一个字符最多占用 3 个字节(一些特殊字符占用 4 个字节,这里先不考虑),所以 767 字节最多能索引 255 个字符。
  • 如果索引的字段长度超过了这个限制,就需要使用前缀索引,只索引字段值的前面一部分。
  • MySQL 5.5 及以下版本会自动截断前缀长度到最大值(255)。
  • MySQL 5.6 及以上版本会直接报错。

所以,为了保证兼容性和避免出错,需要手动指定前缀索引的长度。

解决方案

GORM 和 Hibernate 提供了多种方式来指定索引长度,下面列举几种可行的方案:

1. 使用 column 属性的 length 参数 (推荐)

这是最简单直接的方法,在 mapping 闭包里,通过 column 属性的 length 参数来指定索引长度。

class MyDomain {
    String value

    static constraints = {
        value(blank: false, size: 1..400)
    }

    static mapping = {
        value index: 'value_Idx', column: 'value', length: 255
    }
}

原理:

column 属性用于自定义列的映射,length 参数直接对应于数据库中索引的长度。GORM/Hibernate 会根据这个参数生成相应的 SQL 语句。

代码解释:

  • value index: 'value_Idx':定义了一个名为 value_Idx 的索引。
  • column: 'value':指定索引应用于 value 列。
  • length: 255:设置索引长度为 255。

安全建议:

  • 前缀索引长度不宜过短,可能影响查询效率,一般来说255长度够用了。
  • 如果 value 的实际长度通常都远大于 255,只索引前 255 个字符可能导致索引选择性降低,影响查询性能。需要根据实际情况调整索引长度。
  • 如果数据里有很多以相同前缀开头的值,也可能会影响索引性能。

2. 使用 Hibernate 的 @Column 注解(如果使用注解映射)

如果你的项目使用了注解来配置映射,而不是 GORM 的 DSL,可以使用 Hibernate 的 @Column 注解的 length 属性。

import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Index
import javax.persistence.Table

@Entity
@Table(indexes = {@Index(name = "value_Idx", columnList = "value")})
class MyDomain {

    @Column(length = 255)
    String value

    static constraints = {
        value(blank: false, size: 1..400)
    }
}

原理:

@Column 注解用于定义列的属性,length 属性指定了列的长度,同时也会影响索引的长度(如果该列有索引)。

代码解释:

  • @Entity:标记该类为实体类。
  • @Table(indexes = {@Index(name = "value_Idx", columnList = "value")}): 为 value 字段设置索引,指定 index 名字为 value_idx。
  • @Column(length = 255):设置 value 列的长度为 255,这也会将索引长度限制为 255。

安全建议:

同方案一。

3. 自定义 SQL (不推荐,除非迫不得已)

如果上面两个都不行,最最最笨的方法来了,也可以直接在 BootStrap.groovy 或者某个 service 中执行自定义的 SQL 语句来创建索引,但这通常不推荐,因为这绕过了 GORM/Hibernate 的管理,可能导致不一致。

import groovy.sql.Sql

class BootStrap {

    def dataSource

    def init = { servletContext ->
        def sql = new Sql(dataSource)
        try {
          sql.execute("CREATE INDEX value_Idx ON my_domain (value(255))") // 请替换成自己的表名
        }
        catch(Exception ex){
           //如果已存在,忽略该错误,仅做示范。
        }

        sql.close()
    }
    def destroy = {
    }
}

原理:

直接执行 SQL 语句,绕过 GORM/Hibernate。

代码解释:

  • def dataSource:注入数据源。
  • def sql = new Sql(dataSource):创建 SQL 对象。
  • sql.execute(...):执行创建索引的 SQL 语句。需要根据自己的表名和字段名进行修改。
  • 添加一个异常捕获, 仅用于忽略创建 index 时, index 已经存在的情况. 在实际项目中,应该酌情修改.

安全建议:

  • 确保 SQL 语句正确,避免注入风险。
  • 尽量避免这种方式,除非其他方法都不可行。

进阶: 4-Byte UTF-8字符的考虑.

如果确认你的value字段有很大可能性存入 4-Byte UTF-8 字符(例如 emoji), 为了索引长度最大化, 你需要进一步调整你的索引前缀长度. 因为最大索引长度限制是按照 Byte 计算, 而不是按照字符数.

对于 4-Byte UTF-8字符来说, 最大索引长度: 767 // 4 = 191.

你需要这样设置:

static mapping = {
    value index: 'value_Idx', column: 'value', length: 191
}

在应用代码中,存入数据的时候也最好做好长度检查. 如果确实允许用户存入4-Byte UTF-8字符,又有可能超长,考虑使用TEXT 类型替代 String, 然后增加全文索引。

总结

通过在 GORM 的 mapping 闭包中使用 column 属性的 length 参数,可以很方便地为 MySQL 域属性定义前缀索引长度,从而解决不同版本 MySQL 兼容性的问题, 这是推荐的做法. 其他几种方案要么稍微复杂,要么不推荐。 选择哪种方案, 要看你的项目是怎样的结构. 以及根据需求对索引进行灵活配置.