Go操作SQLite

【译文】原文地址

摘要

关系型数据库的优点在于数据存储方面其提供很多实用的特性。SQLite是一个很好的选择因为其数据库就是一个文件,很容易实现共享数据。即使它就一个文件,SQLite也能处理281Tb的数据。SQLite也有命令行工具sqlite3,可以快速上手使用。
注意:其他数据库有事务,查询语言和schema。然而SQL数据库更趋于成熟和标准化

定义

如下几个名词便于理解:
SQLite:是一个嵌入式SQL数据库。很轻量、快速被广泛使用。也是目前作者最喜欢的一种程序间交互大量数据的一种方式。
事务::在事务中向SQL数据库插入数据。这意味着要么所有数据都插入成功,要么都不成功。事务通过有序性使得数据的重试逻辑更简单。
Schma:数据在关系型数据库中有schma,意味着更容易检查数据的有效性。
SQL:结构化查询语言是查询和修改数据的语言。不需要发明新的筛选数据的方法。SQL是一个构建的格式围绕它有很多内容和工具。

Project项目

我们将用Go写一个HTTP服务器,功能是从交易中获取信息然后存在SQLite数据库中。在GO中,我们将使用github.com/mattn/go-sqlite3,是基于SQLite C库实现的装饰器。
注意:因为go-sqlite使用cgo,初始构建时间会比较长。使用cgo意味着最终的可执行文件会依赖os的共享库,使分发更复杂了点。

Go代码

下面展示的代码在trades.go 文件中可找到。

列表1:Trade结构体

37 // Trade is a buy/sell trade for symbol.
38 type Trade struct {
39     Time   time.Time
40     Symbol string
41     Price  float64
42     IsBuy  bool
43 }

在上面的代码中显示Trade结构体,有时间变量表示交易时间,Symbol代表股票标志(例如AAPL),Price价格和一个布尔类型表示是否购买或卖出。
列表2:数据库schema

24     schemaSQL = `
25 CREATE TABLE IF NOT EXISTS trades (
26     time TIMESTAMP,
27     symbol VARCHAR(32),
28     price FLOAT,
29     buy BOOLEAN
30 );
31 
32 CREATE INDEX IF NOT EXISTS trades_time ON trades(time);
33 CREATE INDEX IF NOT EXISTS trades_symbol ON trades(symbol);
34 `

列表2声明Trade结构对应的数据库schema。25行创建名为trades的表,26-29行定义表的列对应Trade结构的属性。32-33行对表time和symbol列创建索引增加查询速度。
列表3:插入SQL条目

16     insertSQL = `
17 INSERT INTO trades (
18     time, symbol, price, buy
19 ) VALUES (
20     ?, ?, ?, ?
21 )
22 `

列表3定义数据库插入条目的SQL。20行使用“?”为参数占位符。不要使用fmt.sprintf来创建SQL语句,会有SQL注入的安全隐患。

这种一条一条地插入数据可能会很慢。我们将要插入到数据放在缓存中,一旦缓存满了就一次性插入数据库。这样做的好处是快速但如果服务器宕机会有数据丢失的风险。
列表4:DB

45 // DB is a database of stock trades.
46 type DB struct {
47     sql    *sql.DB
48     stmt   *sql.Stmt
49     buffer []Trade
50 }

列表4描述了DB结构体。在47行,我们存放数据库连接。48行存放插入prepared(预编译)语句,49行创建buffer存放需要处理的事务。
列表5:NewDB

52 // NewDB constructs a Trades value for managing stock trades in a
53 // SQLite database. This API is not thread safe.
54 func NewDB(dbFile string) (*DB, error) {
55     sqlDB, err := sql.Open("sqlite3", dbFile)
56     if err != nil {
57         return nil, err
58     }
59 
60     if _, err = sqlDB.Exec(schemaSQL); err != nil {
61         return nil, err
62     }
63 
64     stmt, err := sqlDB.Prepare(insertSQL)
65     if err != nil {
66         return nil, err
67     }
68 
69     db := DB{
70         sql:    sqlDB,
71         stmt:   stmt,
72         buffer: make([]Trade, 0, 1024),
73     }
74     return &db, nil
75 }

列表5展示创建DB来使用数据库。55行我们使用“sqlite”驱动来连接数据库。60行执行SQL schema来建trades表,前提是表不存在。64行我们预编译插入SQL语句。72行创建内部buffer长度0,容量1024。
注意:为了简单,DB API没有提供goroutine安全(不像sql.DB)如果多个goroutines并发调用该API,会出现数据竞争。这个读者可以自行处理。
列表6:Add

77 // Add stores a trade into the buffer. Once the buffer is full, the
78 // trades are flushed to the database.
79 func (db *DB) Add(trade Trade) error {
80     if len(db.buffer) == cap(db.buffer) {
81         return errors.New("trades buffer is full")
82     }
83 
84     db.buffer = append(db.buffer, trade)
85     if len(db.buffer) == cap(db.buffer) {
86         if err := db.Flush(); err != nil {
87             return fmt.Errorf("unable to flush trades: %w", err)
88         }
89     }
90 
91     return nil
92 }

列表6:展示Add方法。84行添加trade实例到buffer中。85行检查buffer是否已满,如果满了就调用Flush,将buffer的数据插入到数据库中去。
列表7:Flush

94  // Flush inserts pending trades into the database.
95  func (db *DB) Flush() error {
96      tx, err := db.sql.Begin()
97      if err != nil {
98          return err
99      }
100 
101     for _, trade := range db.buffer {
102         _, err := tx.Stmt(db.stmt).Exec(trade.Time, trade.Symbol, trade.Price, trade.IsBuy)
103         if err != nil {
104             tx.Rollback()
105             return err
106         }
107     }
108 
109     db.buffer = db.buffer[:0]
110     return tx.Commit()
111 }

列表7:展示Flush方法。96行开始一个事务。101行遍历buffer,102行插入每个trade实例。如果在插入到过程发生错误,104行启动回滚。109行重置内存中的buffer。最后110行,发起事务提交。
列表8:Close

113 // Close flushes all trades to the database and prevents any future trading.
114 func (db *DB) Close() error {
115     defer func() {
116         db.stmt.Close()
117         db.sql.Close()
118     }()
119 
120     if err := db.Flush(); err != nil {
121         return err
122     }
123 
124     return nil
125 }

列表8展示Close方法。在120行,调Flush来插入所有剩余的trades实例到数据库中。116和117行关闭执行语句和数据库。一般创建DB的函数应该有一个defer db.Close()来确保数据的正常关闭和连接释放。
列表9:Imports

4 4 // Your main or test packages require this import so
5 // the sql package is properly initialized.
6 // _ "github.com/mattn/go-sqlite3"
7 
8 import (
9     "database/sql"
10     "errors"
11     "fmt"
12     "time"
13 )

列表9:展示文件的包导入。105行导入database/sql模块是定义SQL数据库的API的。但不包含任何特定的数据库驱动。
列表10:例子

66 func ExampleDB() {
67     dbFile := "/tmp/db-test" + time.Now().Format(time.RFC3339)
68     db, err := trades.NewDB(dbFile)
69     if err != nil {
70         fmt.Println("ERROR: create -", err)
71         return
72     }
73     defer db.Close()
74 
75     const count = 10000
76     for i := 0; i < count; i++ {
77         trade := trades.Trade{
78             Time:   time.Now(),
79             Symbol: "AAPL",
80             Price:  rand.Float64() * 200,
81             IsBuy:  i%2 == 0,
82         }
83         if err := db.Add(trade); err != nil {
84             fmt.Println("ERROR: insert - ", err)
85             return
86         }
87     }
88 
89     fmt.Printf("inserted %d records\n", count)
90     // Output:
91     // inserted 10000 records
92 }

列表10展示了一个例子。67行创建新的数据库和73行defer语句确保数据库关闭。76行启动一个循环来插入trades数据,83行插入trade到数据库中。

你可能感兴趣的:(Go操作SQLite)