27.2.3. 添加新的自定义函数

要使得UDF机制能够起作用,必须使用C或者C++编写函数,你的系统必须支持动态加载。MySQL 源码分发版包括一个sql/udf_example.cc 文件,此文件定义了5个新函数。可以参考这个文件,看UDF是如何调用常规工作。

为了能使用UDF,你需要动态链接mysqld。不要配置MySQL使用--with-mysqld-ldflags=-all-static参数。如果你想使用一个需要从mysqld 访问符号的UDF(例如在使用default_charset_info的sql/udf_example.cc文件中的metaphone函数),你必须使用-rdynamic参数来链接程序(参阅man dlopen)。如果你计划使用UDF, 一个经验法则就是,用with-mysqld-ldflags=-rdynamic设定MySQL,除非你有很好的理由不去这么做。

如果你使用的是预编译分发版的MySQL, 请使用MySQL-Max,其中含有一个动态链接了的服务器,它可以支持动态加载。

对于每个你想要使用在SQL声明中的函数,你应该定义相应的C (或 C++)函数。在下面的讨论中,xxx用来表示范例函数的名字,为了区分使用SQL还是C/C++,xxx()(上标)表示SQL函数调用,xxx()(下标)表示C/C++函数调用。

你为xxx()编写来实现接口的C/C++ 函数如下:

  • xxx() (必有)

    主函数。 这是函数结果被计算的地方。SQL函数数据类型与C/C++函数返回类型的对应关系如下:

    SQL 类型 C/C++ 类型
    STRING char *
    INTEGER long long
    REAL double
  • xxx_init() (可选)

    对xxx()的初始化函数。它可以被用来:

    • 检查传递给xxx()的参量数目。

    • 检查参量是否为必需的类型,或者,除此之外,在主函数被调用的时候告诉MySQL将参量强制为想要的类型。

    • 分配主函数需要的内存。

    • 指定结果的最大长度。

    • 指定(对于REAL 函数)小数的最多位数。

    • 指定结果是否可以为 NULL。

  • xxx_deinit() (可选)

    对xxx()的去初始化函数。它释放初始化函数分配的内存。

当SQL声明调用XXX()时,MySQL调用初始化函数xxx_init(),让它执行必要的设置,比如,检查 参量或分配内存。如果xxx_init() 返回一个错误,SQL声明会退出并给出错误信息,而主函数和去初始化函数并没有被调用。 否则,主函数xxx() 对每一行都被调用一次。所有行都处理完之后,调用去初始化函数xxx_deinit() 执行必要的清除。

对于象SUM()一样工作的集合函数,你也必须提供如下的函数:

  • xxx_clear() (在5.1节中必须)

    对一个新组重置当前集合值为初试集合值,但不插入任何参量。

  • xxx_add() (必须)

    添加参量到当前集合值。

MySQL 按下列操作来处理集合UDF:

  1. 调用 xxx_init() 让集合函数分配它需要用来存储结果的内存。

  2. 按照GROUP BY表达式来排序表。

  3. 为每个新组中的第一行调用xxx_clear()函数。

  4. 为属于同组的每一个新行调用xxx_add()函数。

  5. 当组改变时或每组的最后一行被处理完之后,调用xxx()来获取集合结果。

  6. 重复,以上3-步直到所有行被处理完。

  7. 调用xxx_deinit() 函数去释放UDF分配的内存。.

所有函数必须时线程安全的,这不仅对主函数,对初始化和去初始化函数也一样,也包括集合函数要求的附加函数。这个要求的一个结果就是,你不能分配任何变化的全局或静态变量。如果你需要内存,你可以在xxx_init()函数分配内存,然后在xxx_deinit()函数释放掉。

27.2.3.1. UDF 对简单函数的调用顺序

下面介绍创建简单UDF时需要定义的不同函数。27.2.3节,“添加新的自定义函数”中介绍了MySQL调用这些函数的顺序。

如本节所示,应该说明主函数xxx()。注意返回值和参数会有所不同,这取决于你说明的SQL函数xxx()在CREATE FUNCTION声明中返回的是STRING,INTEGER类型还是REAL类型示:

对于STRING 型函数:

char *xxx(UDF_INIT *initid, UDF_ARGS *args,
          char *result, unsigned long *length,
          char *is_null, char *error);

对于INTEGER型函数:

long long xxx(UDF_INIT *initid, UDF_ARGS *args,
              char *is_null, char *error);

对于REAL型函数:

double xxx(UDF_INIT *initid, UDF_ARGS *args,
              char *is_null, char *error);

初始化和去初始化函数如下说明:

my_bool xxx_init(UDF_INIT *initid, UDF_ARGS *args, char *message);

void xxx_deinit(UDF_INIT *initid);

initid 参数被传递给所有的三个函数。它指向一个UDF_INIT 结构,这个结构被用来在函数之间交换信息。UDF_INIT 结构项跟随着。初始化函数应该给任何它想要改变的项赋值。(要使用项的默认值,就让它不被改变)

  • my_bool maybe_null

    如果xxx() 能返回NULL,xxx_init()应maybe_null 为 1 。如果任一参量被说明了 maybe_null值,其 默认值是1 。

  • unsigned int decimals

    小数位数。默认值是传到主函数的参量里小数的最大位数。(例如,如果函数传递 1.34, 1.345, 和1.3, 那么默认值为,因为1.345 有3位小数。

  • unsigned int max_length

    结果的最大长度。max_length 的默认值因函数的结果类型而异。对字符串函数,默认值是最长参量的长度。对整型函数,默认是21位。对实型函数,默认是13再加上initid->decimals指示的小数位数。(对数字函数,长度包含正负号或者小数点符)。

    如果想返回团值,你可以把max_length 设为从65KB到16MB。这个内存不会被分配,但是如果有临时数据需要存储,这个设置了的值被用来决定使用哪种 列的类型。

  • char *ptr

    函数可以用作本身目的的指针。比如,函数可以用initid->ptr 来在分配了的内存内部通讯。 xxx_init() 应该分配内存,并指派给这个指针:

    initid->ptr = allocated_memory;
    

    在 xxx() 和 xxx_deinit()中,借用 initid->ptr 来使用或分配内存。

27.2.3.2. UDF对集合函数的调用顺序

本节介绍创建集合UDF之时需要定义的不同函数。27.2.3节,“添加新的自定义函数” 介绍了MySQL调用这些函数的顺序。

  • xxx_reset()

    当MySQL在一个新组中发现第一行时调用这个函数。它对这个组重置任何内部总和变量,然后使用给定的UDF_ARGS参量作为内部总和值的第一个值。如下说明 xxx_reset() 函数:

    char *xxx_reset(UDF_INIT *initid, UDF_ARGS *args,
                    char *is_null, char *error);
    

     在MySQL5.1版中UDF接口不需要或不使用xxx_reset()函数,而是使用xxx_clear()函数作为替代。但是如果你想让UDF也能在老版本的服务器上运行,你也可以定义 xxx_reset() 和 xxx_clear() 函数。(如果你使用了这两个函数,xxx_reset()函数在很多情况下可以通过调用函数来内部实现,即调用xxx_clear()函数重置所有变量,然后添加UDF_ARGS参量作为组的第一个值。)

  • xxx_clear()

    当MySQL需要重置总和结果时调用此函数。对每一个新组,在开始之时调用它,但是它也可以被调用来为一个没有匹配行在其中的查询重置值。如下说明xxx_clear():

    char *xxx_clear(UDF_INIT *initid, char *is_null, char *error);
    

    在调用xxx_clear()之前is_null 被设置指向 CHAR(0) 。

    如果发生错误,你可以存储一个值在 error参量指向的变量中。error指向一单字节变量,而不是一个字符串缓冲区。

    xxx_clear() 是MySQL 5.1必须的。

  • xxx_add()

    为同组除了第一行之外,所有的行调用这个函数。你应该用它在UDF_ARGS参量中向内部总和变量加值。.

    char *xxx_add(UDF_INIT *initid, UDF_ARGS *args,
                  char *is_null, char *error);
    

对集合UDF而言xxx() 函数应该用与非集合UDF一样的方法来说明。请参阅27.2.3.1节,“UDF调用简单函数的顺序”

对一个集合UDF,MySQL 在组内所有行被处理之后调用xxx()函数。这里你应该一般不会接触到它的UDF_ARGS参量,但是取而代之地根据内部总和变量返回给你值。

在xxx()中处理的返回值应该用与对非集合UDF一样的方法来操作。请参阅27.2.3.4节,“UDF返回值和错误处理”

xxx_reset() 和 xxx_add() 函数用与非集合UDF一样的方法来处理它们的UDF_ARGS 参量。请参阅27.2.3.3节,“UDF参量处理”

到is_null和error的指针 参量和所有到xxx_reset(), xxx_clear(), xxx_add() 和 xxx()调用一样。你可以用这个来提醒你获取一个错误或无论xxx()是否返回NULL的一个结果。你不能把一个字符串存到*error!error指向单字节变量而不是字符串缓冲区。

*is_null 对每一个组都重置(调用xxx_clear()前), *error 从不重置。

如果 xxx()返回时,*is_null 或 *error 被设置,MySQL返回 NULL作为组函数的结果。

27.2.3.3. UDF参量处理

args 参数指向列着结构元的 UDF_ARGS 结构:

  • unsigned int arg_count

    参量个数。如果你需要你的函数带着某个数目的参量被调用,在初始化函数检查这个值,例如:

    if (args->arg_count != 2)
    {
        strcpy(message,"XXX() requires two arguments");
        return 1;
    }
    
  • enum Item_result *arg_type

    一个指针,对每个参量指向包含类型的一个数列。可能的类型值是STRING_RESULT, INT_RESULT 和 REAL_RESULT。

    要确信一个参量是给定类型的,并且如果不是的话就返回一个错误,请检查初始化函数中的arg_type数列。比如:

    if (args->arg_type[0] != STRING_RESULT ||
        args->arg_type[1] != INT_RESULT)
    {
        strcpy(message,"XXX() requires a string and an integer");
        return 1;
    }
    

    要求你函数的参量是某一类型的另一方法是,使用初始化函数设置arg_type元素为你想要的类型。对所有对xxx()的调用而言,这会导致MySQL强制参量为这些类型。比如,要指定投两个参量强制成字符串和整数,在xxx_init()中分别:

    args->arg_type[0] = STRING_RESULT;
    args->arg_type[1] = INT_RESULT;
    
  • char **args

    args->args 与初始化函数做有关传到你函数的参量的一般情况做通讯。对于常参量i,args->args[i] 指向参量值。(看下面的说明了解如何妥善地访问这个值)。对非-常参量,args->args[i] 为 0。一个常参量为仅使用参量的表达式,如 3 或 4*7-2 或 SIN(3.14)。一个非常 参量是一个行与行不同的表达式,如,列名或带非-常参量调用的函数。

    对主函数的每次调用,args->args 包含为每个当前处理的行传递的实际参量。

    如下使用参量i的函数:

    • 给一个STRING_RESULT 型的参量作为一个字符串加一个长度,可以允许所有二进制数或任意长度的数处理。字符串内容作为args->args[i] 而字符串长度为args->lengths[i]。你不能采用null结尾的字符串。

    • 对一个INT_RESULT型的参量,你必须转换args->args[i] 为一个long long 值:

      long long int_val;
      int_val = *((long long*) args->args[i]);
      
    • 对一个REAL_RESULT型参量,你必须转换args->args[i]为一个双精度值:

      double    real_val;
      real_val = *((double*) args->args[i]);
      
  • unsigned long *lengths

    对初始化函数,lengths数列表示对每个参量的最大字符串长度。你不要改变它。对主函数的每次调用,lengths包含了对当前处理行传递的任何字符串 参量的实际长度。对于INT_RESULT 或 REAL_RESULT类型的参量,lengths 仍包含参量的最大长度(对初始化函数)。

27.2.3.4. UDF返回值和错误处理

如果没有错误发生,初始化函数应该返回0,否则就返回1。如果有错误发生,xxx_init() 应该在message 参数存储一个以null结尾的错误消息。该消息被返回给客户端。消息缓冲区是 MYSQL_ERRMSG_SIZE 字符长度,但你应该试着把消息保持在少于80个字符,以便它能适合标准终端屏幕的宽度。

对于long long 和 double 类型的函数,主函数 xxx()的返回返回值是函数值。字符函数返回一个指向结果的指针,并且设置 *result 和 *length  为返回值的内容和长度。例如:

memcpy(result, "result string", 13);
*length = 13;

被传给 xxx() 函数的结果缓冲区是 255 字节长。如果你的结果适合这个长度,你就不需要担心对结果的内存分配。

如果字符串函数需要返回一个超过255字节的字符串,你必须用 malloc() 在你的 xxx_init() 函数或者 xxx() 函数里为字符串分配空间,并且在 xxx_deinit() 函数里释放此空间。你可以将已分配内存存储在 UDF_INIT  结构里的 ptr  位置以备将来 xxx() 调用。请参阅27.2.3.1节,“UDF 对简单函数的调用顺序”

要在主函数中指明一个 NULL 的返回值,设置 *is_null 为 1 :

*is_null = 1;

要在主函数中指明错误返回,设置 *error 为 1 :

*error = 1;

如果 xxx() 对任意行设置 *error 为 1  ,对于任何 XXX()被调用的语句处理的当前行和随后的任意行,该函数值为 NULL (甚至都不为随后的行调用 xxx())。

27.2.3.5. 编译和安装自定义函数

实现UDF的文件必须在运行服务器的主机上编译和安装。这个步骤在下面介绍,以包含在MySQL源码分发版里的UDF文件sql/udf_example.cc 为例。

紧接着下面的指令是对Unix的,对Windows的指令在本节稍后给出。

 udf_example.cc 文件包含下列函数:

  • metaphon() 返回字符串参量的一个变音位(metaphon)字符串,这有点象一个探测法(soundex)字符串,但是它英语更协调。

  • myfunc_double()返回在其参量中所有字符的ASCII值的和,除以其 参量长度之和。

  • myfunc_int()返回其参量长度之和。

  • sequence([const int]) 返回一个序列,从给定数开始,若没有给定数则从1开始。

  • lookup() 返回对应主机名的IP数。

  • reverse_lookup() 返回对应一个IP数的主机名。函数可以带'xxx.xxx.xxx.xxx'形式的一个单字符串 参量调用,要么带4个数字调用。

一个可动态加载的文件应使用如下这样的命令编译为一个可共享的对象文件:

shell> gcc -shared -o udf_example.so udf_example.cc

如果你使用gcc,你应该能用一个更简单的命令创建udf_example.so :

shell> make udf_example.so

通过运行MySQL源码树下sql里的如下命令,你可以容易地为你的系统决定正确的编译器选项:

shell> make udf_example.o

你应该运行一个类似于make所显示那样的编译命令,除了要在行尾附近删除-c选项,并在行尾加上加上 -o udf_example.so。(在某些系统上,你可能需要在命令行留着-c 选项)。

编译好一个包含有UDF的共享目标后,你必须安装它并通知MySQL。从udf_example.cc编译一个共享目标文件产生一个名字类似于udf_example.so 的文件(确切名字可能因平台而异)。把这个文件复制到 /usr/lib 这样被你系统的动态(运行时)链接器搜索到的目录下,或者 把你放共享目标文件的目录添加到链接器配置文件(如,/etc/ld.so.conf )。

动态链接器的名字时系统特定的(如,在FreeBSD上是ld-elf.so.1 ,在Linux上是 ld.so,在Mac OS X上是dyld )。查看一下你系统的文档,看看链接器的名字是什么及如何配置链接器。

在许多系统上,你也可以设置环境变量LD_LIBRARY 或 LD_LIBRARY_PATH 指向你放UDF的目录。dlopen 手册会告诉你,在你系统上用哪个变量名。你可以在mysql.servermysqld_safe 启动脚本里设置这个然后重启 mysqld

在一些系统上,配置动态链接器的ldconfig不能识别不是以lib做名字开头的共享目标。在这种情况下,你应该把udf_example.so 改名为 libudf_example.so。

在Windows系统上,你可以通过下列步骤编译自定义函数:

  1. 你需要获得BitKeeper source repository for MySQL 5.1。 请参阅 2.8.3节,“从开发源树安装”

  2. 在源数据仓里的VC++Files/examples/udf_example目录下,有名为udf_example.def, udf_example.dsp, 和 udf_example.dsw  的文件。

  3. 在数据仓的sql目录下,复制 udf_example.cc 文件到 VC++Files/examples/udf_example 目录,并改其名为udf_example.cpp。

  4. Visual Studio VC++用打开 udf_example.dsw 文件,用它把UDF编译为一个一般项目。

共享目标文件安装完以后,为新函数信息修改 mysqld ,做如下声明:

mysql> CREATE FUNCTION metaphon RETURNS STRING SONAME 'udf_example.so';
mysql> CREATE FUNCTION myfunc_double RETURNS REAL SONAME 'udf_example.so';
mysql> CREATE FUNCTION myfunc_int RETURNS INTEGER SONAME 'udf_example.so';
mysql> CREATE FUNCTION lookup RETURNS STRING SONAME 'udf_example.so';
mysql> CREATE FUNCTION reverse_lookup
    ->        RETURNS STRING SONAME 'udf_example.so';
mysql> CREATE AGGREGATE FUNCTION avgcost
    ->        RETURNS REAL SONAME 'udf_example.so';

可以使用DROP FUNCTION删除函数:

mysql> DROP FUNCTION metaphon;
mysql> DROP FUNCTION myfunc_double;
mysql> DROP FUNCTION myfunc_int;
mysql> DROP FUNCTION lookup;
mysql> DROP FUNCTION reverse_lookup;
mysql> DROP FUNCTION avgcost;

CREATE FUNCTION 和 DROP FUNCTION 声明更新mysql 数据库中的func 系统表。函数名,类型和共享库名存进表中。你必须有mysql 数据库的INSERT 和DELETE 权限来创建和移除函数。

你不能使用 CREATE FUNCTION 去田间一个先前已经被创建的函数。如果你需要重新安装一个函数,你可以用DROP FUNCTION移除它,然后再用CREATE FUNCTION重新安装它。你可能会需要这么做,比如你重新编译新版本的函数以便mysqld得到这个新版本。不然,服务器还继续使用旧的版本。

一个有效程序是已被 CREATE FUNCTION加载且没有被DROP FUNCTION移除的函数。所有有效函数在每次服务器启动时重新加载,除非你使用--skip-grant-tables选项来启动mysqld。这种情况下,UDF的初始化将被跳过,UDF不可用。

27.2.3.6. 自定义函数安全预防措施

MySQL 采取下列措施来防止误用自定义函数。

你必须有 INSERT 权限才能使用 CREATE FUNCTION 及有 DELETE 权限才能使用 DROP FUNCTION。这是很必要的,因为这些声明在mysql.func表里添加合删除行。

除了对应主 xxx()函数的xxx 符号,UDF应该至少定义一个符号。这些辅助符号对应 xxx_init(), xxx_deinit(), xxx_reset(), xxx_clear() 和 xxx_add() 函数。mysqld 也支持一个控制仅有一个xxx符号的UDF是否被加载的--allow-suspicious-udfs。这个选项 默认是关,以防止从共享目标文件而不是从这些已包含的合法UDF加载的企图。如果你有仅含xxx符号的老版本UDF,以及不能重编译来包含辅助符号的老版本UDF,那就有必要选--allow-suspicious-udfs 选项。否则,你应该避免打开这个选项。

UDF 目标文件不能放在任意目录。它们必须位于动态链接器被配置来搜索到的一些系统目录。为强制执行这个限制并防止指定被动态链接器搜索到的目录之外的路径,MySQL在加载函数的时候检查在CREATE FUNCTION 中指定的共享目标文件名,以及存在mysql.func表中的文件的路径分隔符。这防止通过直接操作mysql.func表指定非法路径名。有关UDF和运行时链接器,请参阅27.2.3.5节,“编译和安装自定义函数”

关注编程学问公众号