扩展mysql - 手把手教你写udf
1 MySQL简介
MySQL是最流行的开放源码SQL数据库管理系统,相对于Oracle,DB2等大型数据库系统,MySQL由于其开源性、易用性、稳定性等特点,受到个人使用者、中小型企业甚至一些大型企业的广泛欢迎,MySQL具有以下特点:
l MySQL是一种关联数据库管理系统,关联数据库将数据保存在不同的表中,而不是将所有数据放在一个大的仓库内,这样就增加了速度并提高了灵活性。
l MySQL软件是一种开放源码软件。
l MySQL数据库服务器具有快速、可靠和易于使用的特点。
l MySQL服务器工作在客户端/服务器模式下,或嵌入式系统中。
l 有大量可用的共享MySQL软件。
2 MySQL内置函数
使用过MySQL的人都知道,MySQL有很多内置函数提供给使用者,包括字符串函数、数值函数、日期和时间函数等,给开发人员和使用者带来了很多方便。下面给几个例子:
l 字符串函数
mysql> select ASCII('2');
+------------+
| ASCII('2') |
+------------+
| 50 |
+------------+
打印字符的ASCII编码。
l 数值函数
mysql> SELECT LOG(10,100);
+-------------+
| LOG(10,100) |
+-------------+
| 2 |
+-------------+
打印以10为底,100的对数值。
l 日期和时间函数
mysql> SELECT CURDATE();
+------------+
| CURDATE() |
+------------+
| 2011-11-11 |
+------------+
打印当前的日期。
这里简单举几个例子,如果想了解MySQL函数的全貌,请访问Mysql官方手册http://dev.mysql.com/doc/#manual.
3 扩展MySQL函数------ UDF
MySQL的内置函数虽然丰富,但毕竟不能满足所有人的需要,有时候我们需要对表中的数据进行一些处理而内置函数不能满足需要的时候,就需要对MySQL进行一些扩展,幸运的是,MySQL给使用者提供了添加新函数的机制,这种使用者自行添加的MySQL函数就称为UDF(User Define Function)。其实除了UDF外,使用者还可以将函数添加为MySQL的固有(内建)函数,固有函数被编译进mysqld服务器中,称为永久可用的,不过这种方式较添加UDF
复杂,升级维护都较为麻烦,这里我们不做讨论。
无论你使用哪种方法去添加新函数,它们都可以被SQL声明调用,就像 ABS()或SUM()这样的固有函数一样。
3.1 UDF的特性
l 函数能返回字符串,整数或实数。
l 你可以定义一次作用于一行的简单函数,或作用于多行的组的集合函数。
l 提供给函数的信息使得函数可以检查传递给它们的参量的数目和类型。
l 你可以让MySQL在将某参量传递给函数之前强制其为某一类型。
l 你可以表示函数返回NULL 或发生错误。
3.2 CREATE FUNCTION/DROP FUNCTION语法
CREATE [AGGREGATE] FUNCTION function_name RETURNS {STRING|INTEGER|REAL}
SONAME shared_library_name
DROP FUNCTION function_name
一个自定义函数 (UDF) 就是用一个象ABS()或SUM()这样的固有(内建)函数一样作用的新函数去扩展MySQL。
function_name 是用在SQL声明中以备调用的函数名字。RETURNS 子句说明函数返回值的类型。shared_library_name 是共享目标文件的基本名,共享目标文件含有实现函数的代码。该文件必须位于一个能被你系统的动态连接者搜索的目录里。
你必须有mysql 数据库的INSERT 权限才能创建一个函数,你必须有mysql 数据库的DELETE权限才能撤销一个函数。这是因为CREATE FUNCTION 往记录函数名字,类型和共享名的mysql.func系统表里添加了一行,而DROP FUNCTION则是从表中删掉这一行。
值得注意的是,要使得UDF机制能够起作用,必须使用C或者C++编写函数,你的系统必须支持动态加载,而且你必须是动态编译的mysqld(非静态)。
3.3 定义UDF
对于每个你想要使用在SQL声明中的函数,你应该定义相应的C(或C++)函数。
你为xxx()编写来实现接口的C/C++函数如下:
l xxx() (必有)
主函数。这是函数结果被计算的地方。SQL函数数据类型与C/C++函数返回类型的对应关系如下:
SQL 类型 |
C/C++ 类型 |
STRING |
char * |
INTEGER |
long long |
REAL |
double |
l xxx_init() (可选)
对xxx()的初始化函数。它可以被用来:
检查传递给xxx()的参量数目。
检查参量是否为必需的类型,或者,除此之外,在主函数被调用的时候告诉MySQL将参量强制为想要的类型。
分配主函数需要的内存。
指定结果的最大长度。
指定(对于REAL 函数)小数的最多位数。
指定结果是否可以为 NULL。
l xxx_deinit() (可选)
对xxx()的去初始化函数。它释放初始化函数分配的内存。
当SQL声明调用XXX()时,MySQL调用初始化函数xxx_init(),让它执行必要的设置,比如,检查参量或分配内存。如果xxx_init()返回一个错误,SQL声明会退出并给出错误信息,而主函数和去初始化函数并没有被调用。否则,主函数xxx()对每一行都被调用一次。所有行都处理完之后,调用去初始化函数xxx_deinit()执行必要的清除。
对于象SUM()一样工作的集合函数,你也必须提供如下的函数:
l xxx_clear()(在5.1版本中必须)
对一个新组重置当前集合值为初试集合值,但不插入任何参量。
l 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()函数释放掉。
3.3.1 主要数据结构
UDF_INIT
typedef struct st_udf_init
{
my_bool maybe_null; /* 1 if function can return NULL */
unsigned int decimals; /* for real functions */
unsigned long max_length; /* For string functions */
char *ptr; /* free pointer for function data */
my_bool const_item; /* 0 if result is independent of arguments */
} UDF_INIT;
l my_bool maybe_null
如果xxx()能返回NULL,xxx_init()应使maybe_null为1。其默认值是1。
l unsigned int decimals
小数位数。默认值是传到主函数的参量里小数的最大位数。(例如,如果函数传递 1.34, 1.345, 和1.3, 那么默认值为3,因为1.345 有3位小数。
l unsigned int max_length
结果的最大长度。max_length的默认值因函数的结果类型而异。对字符串函数,默认值是结果的最大长度。对整型函数,默认是21位。对实型函数,默认是13再加上initid->decimals指示的小数位数。(对数字函数,长度包含正负号或者小数点符)。
如果想返回团值,你可以把max_length 设为从65KB到16MB。这个内存不会被分配,但是如果有临时数据需要存储,这个设置了的值被用来决定使用哪种列的类型。
l char *ptr
函数可以用作本身目的的指针。比如,函数可以用initid->ptr来在分配了的内存内部通讯。 xxx_init()应该分配内存,并指派给这个指针:
initid->ptr = allocated_memory;
在 xxx() 和 xxx_deinit()中,借用initid->ptr来使用或释放内存。
UDF_ARGS
enum Item_result /* 返回结果类型 */
{
STRING_RESULT=0,
REAL_RESULT,
INT_RESULT,
ROW_RESULT,
DECIMAL_RESULT
};
typedef struct st_udf_args
{
unsigned int arg_count; /* Number of arguments */
enum Item_result *arg_type; /* Pointer to item_results */
char **args; /* Pointer to argument */
unsigned long *lengths; /* Length of string arguments */
char *maybe_null; /* Set to 1 for all maybe_null args */
char **attributes; /* Pointer to attribute name */
unsigned long *attribute_lengths;/* Length of attribute arguments */
} UDF_ARGS;
l unsigned int arg_count
参数个数。如果你需要你的函数带着某个数目的参量被调用,在初始化函数检查这个值,例如:
if (args->arg_count != 2)
{
strcpy(message,"XXX() requires two arguments");
return 1;
}
l enum Item_result *arg_type
参数类型列表。要确信一个参量是给定类型的,并且如果不是的话就返回一个错误,请检查初始化函数中的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;
l char **args 参数列表
对主函数的每次调用,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]);
l unsigned long *lengths
对初始化函数,lengths数列表示对每个参量的最大字符串长度。你不要改变它。对主函数的每次调用,lengths包含了对当前处理行传递的任何字符串参量的实际长度。对于INT_RESULT 或 REAL_RESULT类型的参量,lengths仍包含参量的最大长度(对初始化函数)。
3.3.2 简单函数
这里说明简单SQL函数的C/C++主函数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结构,这个结构被用来在函数之间交换信息。
3.3.3 集合函数
这里介绍创建集合UDF之时需要定义的不同函数。
l 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参量作为组的第一个值。)
l 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必须的。
l xxx_add()
为同组所有的行调用这个函数。你应该用它在UDF_ARGS参量中向内部总和变量加值。
char *xxx_add(UDF_INIT *initid, UDF_ARGS *args,
char *is_null, char *error);
对集合UDF而言xxx() 函数应该用与非集合UDF一样的方法来说明。
对一个集合UDF,MySQL在组内所有行被处理之后调用xxx()函数。这里你应该一般不会接触到它的UDF_ARGS参量,但是取而代之地根据内部总和变量返回给你值。
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作为组函数的结果。
3.3.4 错误处理
如果没有错误发生,初始化函数应该返回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() 调用。
要在主函数中指明一个NULL的返回值,设置*is_null为1:
*is_null = 1;
要在主函数中指明错误返回,设置 *error 为 1:
*error = 1;
如果xxx()对任意行设置*error为1 ,对于任何 XXX()被调用的语句处理的当前行和随后的任意行,该函数值为NULL(甚至都不为随后的行调用 xxx())。
4 范例
4.1 编译安装
安装mysql开发包
[root@rocket mysql_udf]# yum -y install mysql-devel
编译udf链接库
代码:udf_str.cpp
#include <mysql.h> #include <mysql_com.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <algorithm> extern "C" { // str_reverse my_bool str_reverse_init(UDF_INIT* initid, UDF_ARGS* args, char* message); void str_reverse_deinit(UDF_INIT* initid); char* str_reverse(UDF_INIT* initid, UDF_ARGS* args, char* result, unsigned long* length, char* is_null, char *error); // LengthAll my_bool mysum_init(UDF_INIT* initid, UDF_ARGS* args, char* message); void mysum_deinit(UDF_INIT* initid); void mysum_reset(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error); void mysum_clear(UDF_INIT *initid, char *is_null, char *error); void mysum_add(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error); long long mysum(UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error); } char* StrData = 0; int gSum = 0; // str_reverse ================================================== my_bool str_reverse_init(UDF_INIT* initid, UDF_ARGS* args, char* message) { if (args->arg_count != 1) { strcpy(message,"wrong number of arguments: str_reverse() requires one argument"); return 1; } if (args->arg_type[0] != STRING_RESULT) { strcpy(message,"str_reverse() requires a string as parameter"); return 1; } StrData = (char*)malloc(4096); memset(StrData, 0, 4096); initid->maybe_null = 1; initid->max_length = 32; initid->ptr = StrData; return 0; } void str_reverse_deinit(UDF_INIT* initid) { free(StrData); } char* str_reverse(UDF_INIT* initid, UDF_ARGS* args, char* result, unsigned long* length, char* is_null, char *error) { if (args->arg_type[0] == STRING_RESULT) { if (strlen(args->args[0]) > 256) { strncpy(StrData, args->args[0], 4096); StrData[4096-1] = 0; std::reverse(StrData, StrData + strlen(StrData)); return StrData; } else { strncpy(result, args->args[0], 256); result[256-1] = 0; std::reverse(result, result + strlen(result)); *length = (unsigned long)strlen(result); return result; } } return NULL; } // LengthAll ================================================== my_bool mysum_init(UDF_INIT* initid, UDF_ARGS* args, char* message) { if (args->arg_count != 1) { strcpy(message,"wrong number of arguments: mysum() requires one argument"); return 1; } if (args->arg_type[0] != INT_RESULT) { strcpy(message,"wrong argument type of arguments: mysum() requires int"); return 1; } gSum = 0; return 0; } void mysum_deinit(UDF_INIT* initid) { } void mysum_reset(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error) { gSum = 0; } void mysum_clear(UDF_INIT *initid, char *is_null, char *error) { gSum = 0; } void mysum_add(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error) { gSum += *(int*)(args->args[0]); } long long mysum(UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error) { return gSum; }
[root@rocket mysql_udf]# g++ -I/usr/include/mysql -shared -fPIC -o udf_str.so udf_str.cpp
查找插件路径
安装插件函数
DROP FUNCTION IF EXISTS str_reverse;
DROP FUNCTION IF EXISTS mysum;
CREATE FUNCTION str_reverse RETURNS string SONAME 'udf_str.so';
CREATE AGGREGATE FUNCTION mysum RETURNS INTEGER SONAME 'udf_str.so';
注意这里的返回值不能写错,不然运行的时候mysql服务器会崩溃!
查看安装结果
4.2 运行
运行str_reverse
运行mysum,先创建一些数据
mysql> create database test;
mysql> use test;
mysql> CREATE TABLE salary( name varchar(64) NOT NULL DEFAULT '' COMMENT 'name', salary int(11) NOT NULL DEFAULT 0 COMMENT 'salary', primary key(name) )ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT 'test';
mysql> insert into salary values ('zhangsan', 11380), ('lisi', 12000), ('wangwu', 8789);
mysql> select mysum(name) from salary;
ERROR 1123 (HY000): Can't initialize function 'mysum'; wrong argument type of arguments: mysum() requires int
这里故意使用name为参数,可以看到我们在程序里打印的错误信息。
执行正确的语句
可以看到mysum实现了和内置函数sum一样的功能。