针对约束条件的适当错误处理,可以使用Go语言和Postgres数据库。

huangapple go评论98阅读模式
英文:

Go pq and Postgres appropriate error handling for constraints

问题

我目前正在使用Go语言的pq库与我的PostgreSQL数据库进行通信。错误检查比预期的要困难一些。通过一个示例场景来描述我的问题可能是最简单的方式。

想象一个Web表单:

用户名  ________
电子邮件  ________
优惠券  ________
密码  ________

一个简单的模式:

username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
voucher VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL

暂时忽略明文密码。如果一个人提交了表单,我可以进行所有的验证来验证约束条件,比如长度/允许的字符等。

现在问题来了,我们要将它放入数据库中,所以我们编写了一个预处理语句并执行它。如果验证正确,唯一可能出错的就是UNIQUE约束。如果有人试图输入一个已经存在的用户名,数据库/SQL将会返回一个错误。

我的问题是我不知道该如何处理这个错误并从中恢复(这应该是一个可恢复的错误)。pq为此提供了一些支持,但仍然存在一些不确定性。

我可以看到两种解决方案,但都不是特别吸引人:

  1. 使用SERIALIZABLE事务,在插入之前检查每个表单值。
  2. 对pq错误结构进行某种形式的解析。

有没有一种常见的模式来实现这样的系统?我希望能够对用户说“对不起,该用户名已存在”,而不是“对不起,发生了一些错误”。

另外,PostgreSQL文档中提到:
> 仅为有限数量的错误类型提供模式名称、表名称、列名称、数据类型名称和约束名称字段;请参见附录A

但是链接的页面对于在数据库对象中返回的值并不是很有帮助。

英文:

I'm currently using the pq lib for Go to communicate with my PostgreSQL database. Error checking is proving to be a little more difficult than anticipated. The easiest way to describe my question is through an example scenario.

Imagine a web form:

Username  ________
Email     ________
Voucher   ________
Password  ________

A rough schema:

username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
voucher VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL

Ignore the presumed plain text password for now. If a person submits the form, I can do all of my validation to verify constraints such as length/allowed characters/etc.

Now it comes to putting it in the database, so we write a prepared statement and execute it. If the validation was done correctly, the only thing that can really go wrong is the UNIQUE constraints. In the event that someone attempts to enter an existing username, database/sql is going to fire back an error.

My problem is that I have no idea what to do with that error and recover from (what should be) a recoverable error. pq provides some support for this, but there still appears to be come ambiguity to what's returned.

I can see two solutions, neither of which sound particularly appealing to me:
A SERIALIZABLE transaction which checks every single form value prior to insertion. Alternatively, some form of parsing on the pq error struct.

Is there a common pattern for implementing such a system? I'd like to be able to say to a user Sorry that username exists rather than Sorry something bad happened

As a sidenote, the PostgreSQL documentation states:
> The fields for
> schema name, table name, column name, data type name, and constraint
> name are supplied only for a limited number of error types; see
> Appendix A.

but the linked page isn't very helpful with respect to values returned in the database object.

答案1

得分: 9

如果验证正确,真正可能出错的只有唯一约束。

不,客户端可能缺乏足够的权限,客户端可能输入了一个有效的密码,但不是“正确”的密码,客户端可能输入了一个有效的凭证,但属于另一个客户端,等等。

使用“在插入之前检查每个表单值的可序列化事务”是没有意义的。只需插入数据,并捕获错误。

至少,您的代码需要检查并响应错误结构中始终存在的C(Code)字段。您不需要解析错误结构,但需要读取它。

如果违反唯一约束,PostgreSQL将在Code字段中返回SQL状态23505。它还将返回第一个违反的约束的名称。它不返回列名,可能是因为唯一约束可以包含多个列。

您可以通过查询information_schema视图来选择约束所引用的列。

这是您表的简单版本。

create table test (
  username VARCHAR(255) UNIQUE NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL,
  voucher VARCHAR(255) UNIQUE NOT NULL,
  password VARCHAR(255) NOT NULL
);

insert into test values ('msherrill', 'me@example.com', 'a', 'wibble');

这个简单而粗糙的Go程序再次插入相同的行。它违反了每个唯一约束。

package main

import (
	"github.com/lib/pq"
	"database/sql"
    "fmt"
    "log"
)

func main() {
	db, err := sql.Open("postgres", "host=localhost port=5435 user=postgres password=xxxxxxxx dbname=scratch sslmode=disable")
	if err != nil {
		log.Fatal(err)
	}

	rows, err := db.Exec("insert into public.test values ('msherrill', 'me@example.com', 'a', 'wibble');")
    if err, ok := err.(*pq.Error); ok {
        fmt.Println("Severity:", err.Severity)
        fmt.Println("Code:", err.Code)
        fmt.Println("Message:", err.Message)
        fmt.Println("Detail:", err.Detail)
        fmt.Println("Hint:", err.Hint)
        fmt.Println("Position:", err.Position)
        fmt.Println("InternalPosition:", err.InternalPosition)
        fmt.Println("Where:", err.Where)
        fmt.Println("Schema:", err.Schema)
        fmt.Println("Table:", err.Table)
        fmt.Println("Column:", err.Column)
        fmt.Println("DataTypeName:", err.DataTypeName)
        fmt.Println("Constraint:", err.Constraint)
        fmt.Println("File:", err.File)
        fmt.Println("Line:", err.Line)
        fmt.Println("Routine:", err.Routine)
    }
   fmt.Println(rows)
}

这是输出。

<pre>
Severity: ERROR
Code: 23505
Message: duplicate key value violates unique constraint "test_username_key"
Detail: Key (username)=(msherrill) already exists.
Hint:
Position:
InternalPosition:
Where:
Schema: public
Table: test
Column:
DataTypeName:
Constraint: test_username_key
File: nbtinsert.c
Line: 406
Routine: _bt_check_unique
</pre>

您有模式、表和约束名称。您可能也知道数据库(目录)名称。使用这些值从information_schema视图中选择模式、表和列名称。您很幸运;在这种情况下,您只需要一个视图。

select table_catalog, table_schema, table_name, column_name 
from information_schema.key_column_usage
where 
    table_catalog = 'scratch' and          -- 数据库名称
    table_schema = 'public' and            -- err.Schema返回的值
    table_name = 'test' and                -- err.Table返回的值
    constraint_name = 'test_username_key'  -- err.Constraint返回的值
order by constraint_catalog, constraint_schema, constraint_name, ordinal_position;
英文:

> If the validation was done correctly, the only thing that can really go wrong is the UNIQUE constraints.

No, the client could lack sufficient privileges, the client might have entered a valid password that's not the right password, the client might have entered a valid voucher that belongs to a different client, etc.

Using "A SERIALIZABLE transaction which checks every single form value prior to insertion" doesn't make sense. Just insert data, and trap errors.

At the very least, your code needs to examine and respond to the C (Code) field, which is always present in the error struct. You don't need to parse the error struct, but you do need to read it.

If you violate a unique constraint, PostgreSQL will return SQL state 23505 in the Code field. It will also return the name of the first constraint that's violated. It doesn't return the column name, probably because a unique constraint can include more than one column.

You can select the column(s) the constraint refers to by querying the information_schema views.

Here's a simple version of your table.

create table test (
  username VARCHAR(255) UNIQUE NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL,
  voucher VARCHAR(255) UNIQUE NOT NULL,
  password VARCHAR(255) NOT NULL
);

insert into test values (&#39;msherrill&#39;, &#39;me@example.com&#39;, &#39;a&#39;, &#39;wibble&#39;);

This quick and dirty go program inserts the same row again. It violates every unique constraint.

package main

import (
	&quot;github.com/lib/pq&quot;
	&quot;database/sql&quot;
    &quot;fmt&quot;
    &quot;log&quot;
)

func main() {
	db, err := sql.Open(&quot;postgres&quot;, &quot;host=localhost port=5435 user=postgres password=xxxxxxxx dbname=scratch sslmode=disable&quot;)
	if err != nil {
		log.Fatal(err)
	}

	rows, err := db.Exec(&quot;insert into public.test values (&#39;msherrill&#39;, &#39;me@example.com&#39;, &#39;a&#39;, &#39;wibble&#39;);&quot;)
    if err, ok := err.(*pq.Error); ok {
        fmt.Println(&quot;Severity:&quot;, err.Severity)
        fmt.Println(&quot;Code:&quot;, err.Code)
        fmt.Println(&quot;Message:&quot;, err.Message)
        fmt.Println(&quot;Detail:&quot;, err.Detail)
        fmt.Println(&quot;Hint:&quot;, err.Hint)
        fmt.Println(&quot;Position:&quot;, err.Position)
        fmt.Println(&quot;InternalPosition:&quot;, err.InternalPosition)
        fmt.Println(&quot;Where:&quot;, err.Where)
        fmt.Println(&quot;Schema:&quot;, err.Schema)
        fmt.Println(&quot;Table:&quot;, err.Table)
        fmt.Println(&quot;Column:&quot;, err.Column)
        fmt.Println(&quot;DataTypeName:&quot;, err.DataTypeName)
        fmt.Println(&quot;Constraint:&quot;, err.Constraint)
        fmt.Println(&quot;File:&quot;, err.File)
        fmt.Println(&quot;Line:&quot;, err.Line)
        fmt.Println(&quot;Routine:&quot;, err.Routine)
    }
   fmt.Println(rows)
}

Here's the output.

<pre>
Severity: ERROR
Code: 23505
Message: duplicate key value violates unique constraint "test_username_key"
Detail: Key (username)=(msherrill) already exists.
Hint:
Position:
InternalPosition:
Where:
Schema: public
Table: test
Column:
DataTypeName:
Constraint: test_username_key
File: nbtinsert.c
Line: 406
Routine: _bt_check_unique
</pre>

You have the schema, table, and constraint names. You presumably know the database (catalog) name, too. Use these values to select the schema, table, and column names from information_schema views. You're lucky; in this case you need only one view.

select table_catalog, table_schema, table_name, column_name 
from information_schema.key_column_usage
where 
    table_catalog = &#39;scratch&#39; and          -- Database name
    table_schema = &#39;public&#39; and            -- value returned by err.Schema
    table_name = &#39;test&#39; and                -- value returned by err.Table
    constraint_name = &#39;test_username_key&#39;  -- value returned by err.Constraint
order by constraint_catalog, constraint_schema, constraint_name, ordinal_position;

huangapple
  • 本文由 发表于 2016年1月23日 19:48:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/34963064.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定