加载中

How to implement PostgreSQL functions and triggers in Golang.

Triggers in PostgreSQL are a simple yet powerful mechanism to react to changes happening in tables.

Read on to find out how to write PostgreSQL triggers in Go.

POSTGRESQL FUNCTIONS AND TRIGGERS

PostgreSQL lets you create user-defined functions using the CREATE FUNCTION SQL statement. Functions are essentially how PostgreSQL can manage user-defined pieces of logic.

Functions can be written in various languages – the most common one is probably PL/pgSQL, which is what you use when you write “stored procedures”. You can also write them in other languages, like Python and Perl.

They can also be written in C. For this, the C code has to be compiled into a dynamically loadable shared library (*.so). PostgreSQL can be told that a function lives as a certain symbol name in a certain *.so file. This is somewhat similar to how modules in Apache or Nginx work.

Functions can be used as triggers, which is what we’re interested in.

怎样应用 PostgreSQL 函数和 Golang 中的触发器?

PostgreSQL 中的触发器是一种简单却功能强大的机制,它能反应表(table)中所正在发生的变化。

下文描述了怎样在 Go 中编写 PostgreSQL 触发器。

POSTGRESQL函数和触发器

通过使用 CREATE FUNCTION SQL 语句,PostgreSQL 可以让你创建用户自定义函数。 函数本质上就是 PostgreSQL 怎样管理逻辑的用户自定义部分。

可以用多种语言编写函数 – 最常见的很可能就是 PL/pgSQL, 它即为编写“stored procedures”时所使用的函数。你也可以用其它语言编写,如 Python 和 Perl

你也可以用 C 代码来编写它们。因此必须将 C 代码编译进一个可动态加载的共享库 (*.so)中。 PostgreSQL 则会被告知,在某个 *.so档中,某个函数会以某个符号名称存在。 这种方式有点类似于Apache 或 Nginx 中模块的工作方式。

函数可以用作触发器,而这点正是我们感兴趣的部分。

TRIGGERS

Triggers are a form of event handlers – they are pieces of logic that can be executed when certain events happen to specified objects. Typically, the objects involved are tables, but they can also be views or foreign tables.

The events, not surprisingly, are:

  • insert (rows)

  • update (rows)

  • delete (rows)

  • truncate (table)

PostgreSQL triggers are versatile:

  • They can be invoked once per row or once per statement. For example, if a statement updates 5 rows, the trigger can be invoked once for the statement or 5 times, one for each row.

  • They can be invoked before or after the actual change happens.

  • The “before” triggers have a change to modify the values or cancel the change.

  • Triggers can be used to impose any arbitrary constraint on a table.

The most popular use of triggers is probably creating audit logs (or more specifically, change logs). You can read more about triggers here and here.

触发器

触发器是事件处理程序的一种形式——它们是当指定对象发生特定事件时可以执行的逻辑块。 通常,它们涉及的对象是表,但它们也可以是视图或外部表。

这些事件通常是:

  • insert (rows)

  • update (rows)

  • delete (rows)

  • truncate (table)

PostgreSQL 触发器用处广泛:

  • 他们可以每行调用一次或每个语句调用一次。例如,如果一条语句更新5行,则可以执行这条语句时调用一次,或者每5行调用,或者每行调用。

  • 可以在实际变化发生之前或之后调用它们。

  • “before”触发器可以更改修改的值或取消更改.

  • 触发器可用于对表施加任意约束。

最受欢迎的触发器的使用可能是创建审计日志(或更具体地说是修改日志)。 您可以在这里这里阅读更多有关触发器的信息。

DYNAMICALLY LOADABLE MODULES IN GO

Starting with version 1.5, Go has the ability to create C-style shared libraries. Using this, you can export an arbitrary Go function that can be invoked by other language runtimes – like dlopen/dlysm in C, ctypes in Python or JNI in Java.

You can build a C-style shared library in Go like this:

go build -o myso.so -buildmode=c-shared myso.go

Here myso.go is a Go main package, which looks like this:

package main

import "C"

//export MyName
func MyName(x int) int {
	return 42 + x
}

func main() {
	// empty
}

Note the “decorator” comment just above the exported function. The import "C" statement is also required for the export to happen.

使用 Go 语言动态加载模组

Go 语言从 1.5 版本开始支持创建 C 语言风格的共享库。这样一来,就可以将任意的 Go 函数导出给其他语言使用,就类似其他语言,例如 C 用的 dlopen/dlysm、Python 的 ctypes,还有 Java 的 JNI 一样。

可以用下面的指令来创建一个 C 风格的共享库:

go build -o myso.so -buildmode=c-shared myso.go

这里的 myso.go 是一个用 Go 写的主包,代码如下:

package main

import "C"

//export MyName
func MyName(x int) int {
	return 42 + x
}

func main() {
	// 未实现
}

注意看导出函数 MyName 上面有一条修饰性注释。另外,如果要导出,代码里还得写 import "C"。

WRITING POSTGRESQL FUNCTIONS IN GO

With this feature, we can build a *.so file contains an exported method that can be invoked as a PostgreSQL function.

There are some conventions that must be adhered to when writing this function – they are detailed here.

Let’s start off by defining the “module”, and listing an exported function called mytrigger.

// file module.go

package main

/*
#include "postgres.h"
#include "fmgr.h"

#cgo LDFLAGS: -Wl,--unresolved-symbols=ignore-all

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

PG_FUNCTION_INFO_V1(mytrigger);
*/
import "C"

func main() {
}

Note the LDFLAGS declaration. This lets us build the so file without the linker complaining about unresolved symbols. For PostgreSQL, there are no libraries to link against, and the symbols that are needed by our shared library can be verified only when the so file is loaded by PostgreSQL.

使用 Go 语言编写 PostgreSQL 函数

可以创建一个包含到处方法的 *.so 文件,然后在里面写要导出的 PostgreSQL 函数。

在写这类函数时,必须遵守一定的规则,详情请点击这里

那么现在,我们先来定义我们的模块,然后写一个叫做 mytrigger 的导出函数。

// file module.go

package main

/*
#include "postgres.h"
#include "fmgr.h"

#cgo LDFLAGS: -Wl,--unresolved-symbols=ignore-all

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

PG_FUNCTION_INFO_V1(mytrigger);
*/
import "C"

func main() {
}

注意代码里的 LDFLAGS 声明。用了之后,链接器在生成 so 文件时,就不会提示遇到了无法解析的符号。因为,我们现在用的是 PostgreSQL,并没有需要链接的外部库;而那些符号,也只在 PostgreSQL 加载 so 文件时才来检查是否存在。

Next, let’s flesh out the trigger function itself in another file mytrigger.go:

// file mytrigger.go

package main

/*
#include "postgres.h"
#include "commands/trigger.h"

//...

*/
import "C"
import (
	"fmt"
	"unsafe"
)

//export mytrigger
func mytrigger(fcInfo *C.FunctionCallInfoData) C.Datum {
	trigdata := (*C.TriggerData)(unsafe.Pointer(fcInfo.context))

	//...
}

The signature of the exported Go function, mytrigger, is mandated by the PostgreSQL function manager convention. In case of triggers, this function is passed the row itself, which it can possibly modify (in case of “before” triggers), and return back.

For now, we’ll create a simple function that will be triggered after INSERTs and UPDATEs. It will not modify the data, and will return it back unchanged. Let’s also assume that the first column in the row will of type “text”, which we’ll read and print.

Now would be a good time to look at how the trigger would look like in C. Here is the example from the PostgreSQL docs.

Within the function, we want to first get the correct row data, since the function can be invoked via an INSERT or an UPDATE:

	var rettuple *C.HeapTupleData
	if C.trigger_fired_by_update(trigdata.tg_event) != 0 {
		rettuple = (*C.HeapTupleData)(trigdata.tg_newtuple)
	} else {
		rettuple = (*C.HeapTupleData)(trigdata.tg_trigtuple)
	}

And then we’ll extract the first column data (indices start from 1), assuming it is a “text” data type (with no embedded NULs):

	url := C.GoString(C.getarg_text(trigdata, rettuple, 1))

We’ll just print it out for now, rather than actually processing it:

	C.elog_info(C.CString(fmt.Sprintf("got url=%s", url)))
	fmt.Println(url)

And finally return the original, unmodified data:

	return C.pointer_get_datum(rettuple)

The full file can be seen here. See below for the github repo link and build instructions.

接着我们在文件 mytrigger.go 中细化触发器函数:

// file mytrigger.go

package main

/*
#include "postgres.h"
#include "commands/trigger.h"

//...

*/
import "C"
import (
	"fmt"
	"unsafe"
)

//export mytrigger
func mytrigger(fcInfo *C.FunctionCallInfoData) C.Datum {
	trigdata := (*C.TriggerData)(unsafe.Pointer(fcInfo.context))

	//...
}

导出的 Go 函数名 mytrigger 由 PostgreSQL 函数管理机制保证唯一。在触发器中,行数据被传入,它可能修改行数据(例如在“before”触发器中),然后将修改后的数据透传下去。

这里我们写一个简单的函数体,在执行 INSERT 和 UPDATE 操作时触发,该函数将不修改数据,只读取和打印它,假设行数据的第一列是“text”类型的。

现在正好看一下触发器函数的代码在 C 中是怎样的,请看 PostgreSQL 文档网址中的这个实例

函数体中,我们先取行数据,因为函数被 INSERT 和 UPDATE 调用:

	var rettuple *C.HeapTupleData
	if C.trigger_fired_by_update(trigdata.tg_event) != 0 {
		rettuple = (*C.HeapTupleData)(trigdata.tg_newtuple)
	} else {
		rettuple = (*C.HeapTupleData)(trigdata.tg_trigtuple)
	}

然后我们取第一列数据(顺序从1开始),假设数据是 “text” 类型的(没有 NULL 列):

	url := C.GoString(C.getarg_text(trigdata, rettuple, 1))

我们只将它打印出来,并不进行修改处理:

	C.elog_info(C.CString(fmt.Sprintf("got url=%s", url)))
	fmt.Println(url)

最后返回原始的未修改过的数据:

	return C.pointer_get_datum(rettuple)

完整的代码可以看这里。下面部分是 github 库的链接和如何生成目标程序的过程指导。

RUNNING THE TRIGGER

To see the trigger in action, first let’s create a table:

$ sudo -u postgres psql -d test
psql (9.6.2)
Type "help" for help.

test=# CREATE TABLE urls ( url TEXT );
CREATE TABLE
test=#

And then our function (you’ll need the USAGE privilege on language C for this):

test=# CREATE FUNCTION mytrigger()
test-# RETURNS TRIGGER AS '/home/alice/ptgo/ptgo.so'
test-# LANGUAGE C;
CREATE FUNCTION
test=#

Next let’s create a trigger on INSERT and UPDATE on table urls, that invokes our function:

test=# CREATE TRIGGER trig_1
test-# AFTER INSERT OR UPDATE
test-# ON urls
test-# FOR EACH ROW
test-# EXECUTE PROCEDURE mytrigger();
CREATE TRIGGER
test=#

Now let’s insert a couple of rows. The “got url=” lines are printed by our function:

test=# INSERT INTO urls VALUES ('http://example.com/');
INFO:  got url=http://example.com/
INSERT 0 1
test=#
test=# INSERT INTO urls VALUES ('http://mydomain.com/');
INFO:  got url=http://mydomain.com/
INSERT 0 1
test=#

And when the rows are updated, the function receives the post-change values because it is an AFTER trigger:

test=# UPDATE urls SET url='http://www.test.com/';
INFO:  got url=http://www.test.com/
INFO:  got url=http://www.test.com/
UPDATE 2
test=#

And that’s it! We have our very own PostgreSQL trigger written in Go!

运行触发器

在触发器运行之前,先创建表:

$ sudo -u postgres psql -d test
psql (9.6.2)
Type "help" for help.

test=# CREATE TABLE urls ( url TEXT );
CREATE TABLE
test=#

之后,是创建我们的函数(你需要有 C 语言的 USAGE 权限,像这样):

test=# CREATE FUNCTION mytrigger()
test-# RETURNS TRIGGER AS '/home/alice/ptgo/ptgo.so'
test-# LANGUAGE C;
CREATE FUNCTION
test=#

下一步,让我们创建一个在表路径上的 INSERT 和 UPDATE 触发器,并让它来调用我们的函数:

test=# CREATE TRIGGER trig_1
test-# AFTER INSERT OR UPDATE
test-# ON urls
test-# FOR EACH ROW
test-# EXECUTE PROCEDURE mytrigger();
CREATE TRIGGER
test=#

现在,让我们插入两行。“got url=” 通过我们的函数被打印出来:

test=# INSERT INTO urls VALUES ('http://example.com/');
INFO:  got url=http://example.com/
INSERT 0 1
test=#
test=# INSERT INTO urls VALUES ('http://mydomain.com/');
INFO:  got url=http://mydomain.com/
INSERT 0 1
test=#

当行被更新的时候,函数接收更新后的值,因为它是一个 AFTER 触发器:

test=# UPDATE urls SET url='http://www.test.com/';
INFO:  got url=http://www.test.com/
INFO:  got url=http://www.test.com/
UPDATE 2
test=#

就是这样!我们有了我们自己的用 Go 编写的 PostgreSQL 触发器!

THE CODE

The entire code is available on GitHub here: github.com/rapidloop/ptgo. Feel free to fork it and modify it to implement your own triggers. It has been tested only on Linux. To get started, do:

git clone https://github.com/rapidloop/ptgo
cd ptgo
make

You might need to install the development package for Postgres first. For Debian-based systems, this can be done with:

sudo apt-get install postgresql-server-dev-9.6

代码

所有代码可以在GitHub上访问,链接如下:github.com/rapidloop/ptgo。你可随意 fork 并修改以实现你自己的触发器。它仅在 Linux 上测试过。请按照下面步骤获取源码:

git clone https://github.com/rapidloop/ptgo
cd ptgo
make

你可能需要首先安装 Postgres 开发包。对于基于 Debian 的系统,可以使用下面命令:

sudo apt-get install postgresql-server-dev-9.6
返回顶部
顶部