【译文】原文地址
摘要
关系型数据库的优点在于数据存储方面其提供很多实用的特性。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到数据库中。