在Golang Web服务器中何时准备PostgreSQL语句?

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

when to prepare postgresql statements in a golang web server?

问题

我有一个连接到PostgreSQL数据库的Web服务器。据我了解,PostgreSQL驱动程序在内部管理连接池,所以我将数据库连接设置为全局变量。

我正在使用预处理语句,但我不知道在服务器启动之前是否应该在我的main函数中提前准备好它们,还是在我的请求处理程序中准备(如下所示)。我对Go语言还不熟悉。我认为将语句设置为全局变量更高效,但我不确定。请帮忙看看。

var db *sql.DB

func main() {
  router = pat.New()
  router.Get("/", handler)
  db, e := sql.Open("postgres", "...")
  ...
  http.ListenAndServe("127.0.0.1", router)
}

func handler(w http.ResponseWriter, r *http.Request) {
  s, e := db.Prepare("select * from mytable where field=$1")
  r, e := s.Exec(123)
  ...
}
英文:

I have a web server that connects to a postgresql database. As I understand, postgresql driver manages a connection pool internally so I made the database connection a global variable.

I am using prepared statements and I do not know whether it is a good idea to prepare them in advance in my main function before the server has started, or do it in my request handlers (as below). I am new to golang. I think it is more efficient to make the statements global, but I am not sure. Please help.

var db *sql.DB

func main() {
  router = pat.New()
  router.Get("/", handler)
  db, e := sql.Open("postgres", "...")
  ...
  http.ListenAndServe("127.0.0.1", router)
}

func handler(w http.ResponseWriter, r *http.Request) {
  s, e := db.Prepare("select * from mytable where field=$1")
  r, e := s.Exec(123)
  ...
}

答案1

得分: 10

这完全取决于你的使用情况。作为一个经验法则,我会说你应该在运行服务器之前准备好你的语句,原因有以下几点:

  • 如果一个语句没有正确准备,你可能无法启动。如果在需要时才准备语句,一个失败的语句可能会在程序启动后很久才使整个程序无效。
  • 如果事先准备好,你就不必处理并发性:如果在需要时准备,你必须使用同步机制来确保一个语句不会在并行中被多次准备,否则你的SQL服务器可能会崩溃。
  • 这样处理起来更简单。

至于数据库句柄,你应该将语句设置为全局变量,这样可以随时方便地使用,而不必传递指针。如果你发现自己需要处理很多语句,比如超过10-15个(任意数字),你可能会发现将所有与数据库相关的内容(数据库初始化、查询等)放入主包的子包中更容易管理。

英文:

It all depends on your use case. As a rule of thumb, I would say that you should prepare your statements before running your server, for multiple reasons:

  • You can fail to start if a statement doesn't prepare correctly. If you prepare them on-the-fly, a failing statement could invalid the entire program long after it has been started.
  • You don't have to handle concurrency if you prepare them beforehand: if preparing when needed, you have to use a syncing mechanism to ensure a statement won't be prepared multiple times in parallel, which would end with your SQL server going down in flames…
  • It is way simpler to handle.

As for your database handle, you should make the statements global to have them handy at any time without having to pass pointers around. If you find yourself juggling with many statements, like more than 10-15 (arbitrary number), you will probably find it easier to put all DB-related stuff (DB initialization, queries, etc) into a sub-package of your main package.

答案2

得分: 3

准备语句的优势

我尝试在应用程序启动时准备所有的语句。这样做可以带来一些性能上的好处,但正如其他答案中提到的,这会检查语句的有效性,并可能在启动时失败。结合一些自愈机制,比如在 Kubernetes 上,我能够避免部署一个有问题的语句(由于额外的逗号),并回滚到我应用程序的已知版本。

全局变量的缺点

尽管如此,我不建议使用全局变量来管理你的准备语句。这些变量很难进行测试,并且会使重构和扩展应用程序变得更加困难。相反,我使用以下模式:

定义一个服务接口

通过定义一个可以传递给函数的接口,我们可以轻松地为测试创建一个模拟实现。

// ExampleRetrievalService 定义了检索示例的方法
type ExampleRetrievalService interface {
    // All 返回特定用户ID的所有示例
    All(userID string) ([]Example, error)
}

http.Handler 中接受接口

通过编写一个接受接口的处理程序,我们可以轻松地传入不同的实现,例如 postgresredismysql 等。

// ExampleHandler 接受 ExampleRetrievalService 的实现,并使用它返回 Example 对象的列表
func ExampleHandler(exampleService ExampleRetrievalService) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        examples, err := exampleService.All(getUserID(r))
        [...]
    })
}

使用数据库实现服务

postgres 包中,我使用一个带有数据库支持的结构来实现服务。注意,属性是准备语句

type exampleRetrievalService struct {
    retrieveAllStatement *sqlx.Stmt
}

func (s *exampleRetrievalService) All(userID string) ([]ex.Example, error) {
    var examples []ex.Example
    return examples, s.retrieveAllStatement.Select(&examples, userID)
}

在服务实现中创建和准备语句

const (
    selectAllForUserID = `SELECT * FROM example WHERE user_id=$1`
)

// NewExampleRetrievalService 接受一个 `*sqlx.DB`,并返回一个由 PostgreSQL 支持的 `ex.ExampleRetrievalService` 实现
func NewExampleRetrievalService(db *sqlx.DB) (*exampleRetrievalService, error) {
    retrieveAllStatement, err := db.Preparex(selectAllForUserID)

    if err != nil {
        errors.Wrap(err, "unable to prepare database transactions")
    }
    return &exampleRetrievalService{
        retrieveAllStatement: retrieveAllStatement,
    }, err
}

使用数据库创建函数

在我的 main.go 中,我只需要创建一个新的服务实现,并将其传递给相关的函数。现在可以使用模拟实现进行测试,并且如果需要,可以轻松地按函数切换实现。

func main() {
    [...]
    db = sqlx.MustConnect("postgres", viper.GetString(keyPostgresConn))
    exampleRetrievalService, err = postgres.NewExampleRetrievalService(db)
    [...]
    r.Handle("/", ExampleHandler(exampleRetrievalService))
}

在服务初始化时使用准备语句可以确保我的 SQL 语句至少是正确的。通过定义传递给处理程序的接口而不是全局变量,意味着我可以为集成测试创建测试 postgres 实现。这可能看起来有点额外的工作,但这种结构提供了处理简单和非常复杂系统的足够灵活性。

英文:

Note this is a longer article based on my readings and personal experience and will the form the basis of a blog article I am writing. I will try to explain the concepts with examples from an API service I am developing

Advantages of Prepared Statements

I try to prepare all my statements upon startup of my application. This can have some performance benefits but as mentioned in the other answer this checks the statement is valid and can fail startup. Combined with some self healing like on Kubernetes I have been able to avoid deploying a broken statement (due to an additional ,) and roll back to a known version of my application.

Disadvantages of global variables

Despite this I would not recommend using global variables to manage your prepared statements. These are difficult to test and make refactoring and expanding your application harder. Instead I use the pattern below:

Define a service interface

By defining an interface that can be passed to functions we can easily mock an implementation for testing.

// ExampleRetrievalService defines methods for retrieval
// of examples
type ExampleRetrievalService interface {
	// All returns all examples for a particular userID
	All(userID string) ([]Example, error)
}

Accept the interface in your http.Handler

By writing a handler that accepts an interface we can easily pass in different implementations. e.g. a postgres backed implementation or a redis or mysql etc.

// ExampleHandler takes an implementation of ExampleRetrievalService and uses it to return a list of Example objects
func ExampleHandler(exampleService ExampleRetrievalService) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        examples, err := exampleService.All(getUserID(r))
        [...]
	})
}

Implement service with database

In a postgres package I implement the service with a database backed struct. Note the attributes are prepared statements:

type exampleRetrievalService struct {
	retrieveAllStatement    *sqlx.Stmt
}

func (s *exampleRetrievalService) All(userID string) ([]ex.Example, error) {
	var examples []ex.Example
	return examples, s.retrieveAllStatement.Select(&examples, userID)
}

Upon service implementation create, prepare statements

const (
    selectAllForUserID = `SELECT * FROM example WHERE user_id=$1`
)

// NewExampleRetrievalService takes a `*sqlx.DB` and returns an implementation of
// `ex.ExampleRetrievalService` backed by PostgreSQL
func NewExampleRetrievalService(db *sqlx.DB) (*exampleRetrievalService, error) {

	retrieveAllStatement, err := db.Preparex(selectAllForUserID)

	if err != nil {
		errors.Wrap(err, "unable to prepare database transactions")
	}
	return &exampleRetrievalService{
		retrieveAllStatement:    retrieveAllStatement,
	}, err
}

Use the database creation function

In my main.go all I have to do is create a new implementation of my service and pass it to relevant functions. These can now be tested with mock implementations and I can easily swap implementations per function if required.

func main() {
    [...]
    db = sqlx.MustConnect("postgres", viper.GetString(keyPostgresConn))
	exampleRetrievalService, err = postgres.NewExampleRetrievalService(db)
    [...]
    r.Handle("/", ExampleHandler(exampleRetrievalService))
}

Using prepared statements upon service initialisation guarantees at least my SQL statements are correct. Defining interfaces that I pass to handlers instead of global variables means I can create test postgres implementations for integration testing. It may seem a little extra work but this structure provides plenty of flexibility to handle both simple and very complex systems.

huangapple
  • 本文由 发表于 2014年7月4日 14:31:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/24568040.html
匿名

发表评论

匿名网友

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

确定