使用 SQLite 优化番茄钟应用:测试、更新与数据展示
1. 用 SQLite 测试存储库
在之前对番茄钟功能进行测试时,我们使用辅助函数
getRepo
来获取存储库,当时只有内存存储库可用。现在添加了 SQLite3 存储库,我们需要提供该函数的另一个版本,以便返回新的存储库。我们可以通过应用构建标签来控制何时使用哪个存储库。
操作步骤如下:
1. 切换到
pomodoro
包目录,创建一个名为
sqlite3_test.go
的文件:
$ cd $HOME/pragprog.com/rggo/persistentDataSQL/pomo/pomodoro
-
打开
sqlite3_test.go文件,添加构建标签+build !inmemory,并添加包定义:
//+build !inmemory
package pomodoro_test
- 添加导入部分:
import (
"io/ioutil"
"os"
"testing"
"pragprog.com/rggo/interactiveTools/pomo/pomodoro"
"pragprog.com/rggo/interactiveTools/pomo/pomodoro/repository"
)
-
定义
getRepo函数,该函数将返回存储库实例和清理函数:
func getRepo(t *testing.T) (pomodoro.Repository, func()) {
t.Helper()
tf, err := ioutil.TempFile("", "pomo")
if err != nil {
t.Fatal(err)
}
tf.Close()
dbRepo, err := repository.NewSQLite3Repo(tf.Name())
if err != nil {
t.Fatal(err)
}
return dbRepo, func() {
os.Remove(tf.Name())
}
}
- 保存并关闭文件,再次执行测试以测试新的存储库:
$ go test
若要使用内存存储库执行测试,可在测试命令中提供
inmemory
标签:
$ go test -tags=inmemory
测试结果不会显示使用了哪个存储库后端,因为测试依赖于更高级的
Repository
接口。如果想确保测试使用的是 SQLite 存储库,可以监控临时目录,验证是否创建了符合
pomo211866403
模式的临时数据库文件;或者在
getRepo
函数中使用
t.Log
方法打印消息,如
Using SQLite repository
,以提供一些快速的可视化反馈。
2. 更新应用以使用 SQLite 存储库
当 SQLite 存储库可用后,需要更新 Pomo 应用以使用它。操作步骤如下:
1. 切换到
cmd
目录:
$ cd $HOME/pragprog.com/rggo/persistentDataSQL/pomo/cmd
-
编辑
root.go文件,添加一个新的命令行参数,让用户可以指定要使用的数据库文件,并将该标志与viper绑定:
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
"config file (default is $HOME/.pomo.yaml)")
rootCmd.Flags().StringP("db", "d", "pomo.db", "Database file")
rootCmd.Flags().DurationP("pomo", "p", 25*time.Minute,
"Pomodoro duration")
rootCmd.Flags().DurationP("short", "s", 5*time.Minute,
"Short break duration")
rootCmd.Flags().DurationP("long", "l", 15*time.Minute,
"Long break duration")
viper.BindPFlag("db", rootCmd.Flags().Lookup("db"))
viper.BindPFlag("pomo", rootCmd.Flags().Lookup("pomo"))
viper.BindPFlag("short", rootCmd.Flags().Lookup("short"))
viper.BindPFlag("long", rootCmd.Flags().Lookup("long"))
}
-
创建一个新文件
reposqlite.go,添加构建标签+build !inmemory和包定义:
// +build !inmemory
package cmd
- 添加导入部分:
import (
"github.com/spf13/viper"
"pragprog.com/rggo/interactiveTools/pomo/pomodoro"
"pragprog.com/rggo/interactiveTools/pomo/pomodoro/repository"
)
-
定义
getRepo函数,根据配置的数据库文件名返回存储库实例:
func getRepo() (pomodoro.Repository, error) {
repo, err := repository.NewSQLite3Repo(viper.GetString("db"))
if err != nil {
return nil, err
}
return repo, nil
}
- 保存文件,切换回应用的根目录,构建应用以使用新的存储库进行测试:
$ cd ..
$ go build
-
运行应用并使用
--help查看新的--db选项:
$ ./pomo --help
默认情况下,如果未指定,
pomo
会创建并使用一个名为
pomo.db
的数据库文件。执行应用以查看是否创建了该文件:
$ ./pomo
打开一个新终端,切换到应用的根目录,检查
pomo.db
文件是否存在:
$ cd $HOME/pragprog.com/rggo/persistentDataSQL/pomo
$ ls pomo.db
使用
sqlite3
客户端连接到该数据库,执行
SELECT
查询:
$ sqlite3 pomo.db
sqlite> select * from interval;
由于应用刚刚创建了这个数据库和表,查询应该返回空结果。切换回原终端,使用应用的
Start
按钮开始一个时间段,再切换回运行
sqlite3
客户端的终端,重新执行相同的查询,会看到有新的条目出现。
3. 向用户显示摘要信息
将数据存储在 SQL 数据库中的一个好处是,可以使用其强大的查询功能以多种方式对数据进行查询和汇总。为了向用户展示活动摘要,我们将在应用界面中添加两个新部分:
-
每日摘要部分
:使用
BarChart
Termdash 小部件展示当前一天按番茄钟和休息时间细分的活动摘要(以分钟为单位)。
-
每周摘要部分
:使用
LineChart
Termdash 小部件展示当前一周按番茄钟和休息时间细分的活动情况。
操作步骤如下:
1. 修改
Repository
接口:
- 切换到
pomodoro
目录:
$ cd $HOME/pragprog.com/rggo/persistentDataSQL/pomo/pomodoro
- 编辑 `interval.go` 文件,向 `Repository` 接口添加一个新方法 `CategorySummary`:
type Repository interface {
Create(i Interval) (int64, error)
Update(i Interval) error
ByID(id int64) (Interval, error)
Last() (Interval, error)
Breaks(n int) ([]Interval, error)
CategorySummary(day time.Time, filter string) (time.Duration, error)
}
-
实现新方法:
-
内存存储库
:打开
repository/inMemory.go文件,添加CategorySummary方法:
-
内存存储库
:打开
func (r *inMemoryRepo) CategorySummary(day time.Time,
filter string) (time.Duration, error) {
// Return a daily summary
r.RLock()
defer r.RUnlock()
var d time.Duration
filter = strings.Trim(filter, "%")
for _, i := range r.intervals {
if i.StartTime.Year() == day.Year() &&
i.StartTime.YearDay() == day.YearDay() {
if strings.Contains(i.Category, filter) {
d += i.ActualDuration
}
}
}
return d, nil
}
- **SQLite3 存储库**:打开 `repository/sqlite3.go` 文件,添加 `CategorySummary` 方法:
func (r *dbRepo) CategorySummary(day time.Time,
filter string) (time.Duration, error) {
// Return a daily summary
r.RLock()
defer r.RUnlock()
stmt := `SELECT sum(actual_duration) FROM interval
WHERE category LIKE ? AND
strftime('%Y-%m-%d', start_time, 'localtime')=
strftime('%Y-%m-%d', ?, 'localtime') `
var ds sql.NullInt64
err := r.db.QueryRow(stmt, filter, day).Scan(&ds)
var d time.Duration
if ds.Valid {
d = time.Duration(ds.Int64)
}
return d, err
}
-
添加生成小部件所需数据的函数:
-
在
pomodoro目录下创建并编辑summary.go文件,添加包定义和导入部分:
-
在
package pomodoro
import (
"fmt"
"time"
)
- 定义 `DailySummary` 函数:
func DailySummary(day time.Time,
config *IntervalConfig) ([]time.Duration, error) {
dPomo, err := config.repo.CategorySummary(day, CategoryPomodoro)
if err != nil {
return nil, err
}
dBreaks, err := config.repo.CategorySummary(day, "%Break")
if err != nil {
return nil, err
}
return []time.Duration{
dPomo,
dBreaks,
}, nil
}
- 定义自定义类型 `LineSeries` 和 `RangeSummary` 函数:
type LineSeries struct {
Name string
Labels map[int]string
Values []float64
}
func RangeSummary(start time.Time, n int,
config *IntervalConfig) ([]LineSeries, error) {
pomodoroSeries := LineSeries{
Name: "Pomodoro",
Labels: make(map[int]string),
Values: make([]float64, n),
}
breakSeries := LineSeries{
Name: "Break",
Labels: make(map[int]string),
Values: make([]float64, n),
}
for i := 0; i < n; i++ {
day := start.AddDate(0, 0, -i)
ds, err := DailySummary(day, config)
if err != nil {
return nil, err
}
label := fmt.Sprintf("%02d/%s", day.Day(), day.Format("Jan"))
pomodoroSeries.Labels[i] = label
pomodoroSeries.Values[i] = ds[0].Seconds()
breakSeries.Labels[i] = label
breakSeries.Values[i] = ds[1].Seconds()
}
return []LineSeries{
pomodoroSeries,
breakSeries,
}, nil
}
-
更新应用界面以显示新的小部件:
-
切换到
app目录:
-
切换到
$ cd ../app
- 创建并编辑 `summaryWidgets.go` 文件,添加包定义和导入部分:
package app
import (
"context"
"math"
"time"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/widgets/barchart"
"github.com/mum4k/termdash/widgets/linechart"
"pragprog.com/rggo/interactiveTools/pomo/pomodoro"
)
- 定义 `summary` 类型及其 `update` 方法:
type summary struct {
bcDay *barchart.BarChart
lcWeekly *linechart.LineChart
updateDaily chan bool
updateWeekly chan bool
}
func (s *summary) update(redrawCh chan<- bool) {
s.updateDaily <- true
s.updateWeekly <- true
redrawCh <- true
}
- 定义 `newSummary` 函数:
func newSummary(ctx context.Context, config *pomodoro.IntervalConfig,
redrawCh chan<- bool, errorCh chan<- error) (*summary, error) {
s := &summary{}
var err error
s.updateDaily = make(chan bool)
s.updateWeekly = make(chan bool)
s.bcDay, err = newBarChart(ctx, config, s.updateDaily, errorCh)
if err != nil {
return nil, err
}
s.lcWeekly, err = newLineChart(ctx, config, s.updateWeekly, errorCh)
if err != nil {
return nil, err
}
return s, nil
}
- 定义 `newBarChart` 函数:
func newBarChart(ctx context.Context, config *pomodoro.IntervalConfig,
update <-chan bool, errorCh chan<- error) (*barchart.BarChart, error) {
bc, err := barchart.New(
barchart.ShowValues(),
barchart.BarColors([]cell.Color{
cell.ColorBlue,
cell.ColorYellow,
}),
barchart.ValueColors([]cell.Color{
cell.ColorBlack,
cell.ColorBlack,
}),
barchart.Labels([]string{
"Pomodoro",
"Break",
}),
)
if err != nil {
return nil, err
}
updateWidget := func() error {
ds, err := pomodoro.DailySummary(time.Now(), config)
if err != nil {
return err
}
return bc.Values(
[]int{int(ds[0].Minutes()),
int(ds[1].Minutes())},
int(math.Max(ds[0].Minutes(),
ds[1].Minutes())*1.1)+1,
)
}
go func() {
for {
select {
case <-update:
errorCh <- updateWidget()
case <-ctx.Done():
return
}
}
}()
if err := updateWidget(); err != nil {
return nil, err
}
return bc, nil
}
- 定义 `newLineChart` 函数:
func newLineChart(ctx context.Context, config *pomodoro.IntervalConfig,
update <-chan bool, errorCh chan<- error) (*linechart.LineChart, error) {
lc, err := linechart.New(
linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)),
linechart.YLabelCellOpts(cell.FgColor(cell.ColorBlue)),
linechart.XLabelCellOpts(cell.FgColor(cell.ColorCyan)),
linechart.YAxisFormattedValues(
linechart.ValueFormatterSingleUnitDuration(time.Second, 0),
),
)
if err != nil {
return nil, err
}
updateWidget := func() error {
ws, err := pomodoro.RangeSummary(time.Now(), 7, config)
if err != nil {
return err
}
err = lc.Series(ws[0].Name, ws[0].Values,
linechart.SeriesCellOpts(cell.FgColor(cell.ColorBlue)),
linechart.SeriesXLabels(ws[0].Labels),
)
if err != nil {
return err
}
return lc.Series(ws[1].Name, ws[1].Values,
linechart.SeriesCellOpts(cell.FgColor(cell.ColorYellow)),
linechart.SeriesXLabels(ws[1].Labels),
)
}
go func() {
for {
select {
case <-update:
errorCh <- updateWidget()
case <-ctx.Done():
return
}
}
}()
if err := updateWidget(); err != nil {
return nil, err
}
return lc, nil
}
-
集成小部件到应用中:
-
编辑
grid.go文件,更新newGrid定义以包含摘要小部件:
-
编辑
func newGrid(b *buttonSet, w *widgets, s *summary,
t terminalapi.Terminal) (*container.Container, error) {
// Add third row
builder.Add(
grid.RowHeightPerc(60,
grid.ColWidthPerc(30,
grid.Widget(s.bcDay,
container.Border(linestyle.Light),
container.BorderTitle("Daily Summary (minutes)"),
),
),
grid.ColWidthPerc(70,
grid.Widget(s.lcWeekly,
container.Border(linestyle.Light),
container.BorderTitle("Weekly Summary"),
),
),
),
)
}
- 编辑 `buttons.go` 文件,更新 `newButtonSet` 函数以包含摘要集合,并在结束回调函数中添加对 `s.update` 的调用:
func newButtonSet(ctx context.Context, config *pomodoro.IntervalConfig,
w *widgets, s *summary,
redrawCh chan<- bool, errorCh chan<- error) (*buttonSet, error) {
end := func(pomodoro.Interval) {
w.update([]int{}, "", "Nothing running...", "", redrawCh)
s.update(redrawCh)
}
}
- 编辑 `app.go` 文件,更新 `New` 函数以实例化新的摘要小部件集合,并将其传递给 `newButtonSet` 和 `newGrid`:
w, err := newWidgets(ctx, errorCh)
if err != nil {
return nil, err
}
s, err := newSummary(ctx, config, redrawCh, errorCh)
if err != nil {
return nil, err
}
b, err := newButtonSet(ctx, config, w, s, redrawCh, errorCh)
if err != nil {
return nil, err
}
term, err := tcell.New()
c, err := newGrid(b, w, s, term)
- 切换回应用的根目录,构建新应用进行测试:
$ cd ..
$ go build
运行应用以查看新的小部件。如果没有保存历史记录,它们最初可能为空。运行几个时间段后,会看到两个小部件在每个时间段结束时用摘要数据进行更新。
若要使用内存数据存储编译应用,可在
go build
命令中使用
-tags=inmemory
参数。应用在打开时会显示汇总数据,但关闭后会重置为空,因为内存中的数据会丢失。
通过使用交互式小部件和 SQL 数据库,我们成功构建了一个强大的命令行番茄钟应用。
使用 SQLite 优化番茄钟应用:测试、更新与数据展示(续)
4. 总结与回顾
为了更清晰地理解整个优化过程,我们可以通过一个流程图来展示从测试存储库到更新应用并显示摘要信息的主要步骤:
graph LR
A[测试 SQLite 存储库] --> B[更新应用使用 SQLite 存储库]
B --> C[向用户显示摘要信息]
C --> D[集成小部件到应用]
D --> E[构建并测试应用]
以下是整个过程的关键步骤总结表格:
| 步骤 | 操作内容 | 文件 | 代码片段 |
| — | — | — | — |
| 测试存储库 | 创建
sqlite3_test.go
文件,定义
getRepo
函数 |
sqlite3_test.go
| 见前文
getRepo
函数代码 |
| 更新应用 | 编辑
root.go
添加命令行参数,创建
reposqlite.go
定义
getRepo
函数 |
root.go
,
reposqlite.go
| 见前文对应代码 |
| 显示摘要信息 | 修改
Repository
接口,实现
CategorySummary
方法,添加生成小部件数据的函数 |
interval.go
,
repository/inMemory.go
,
repository/sqlite3.go
,
summary.go
| 见前文对应代码 |
| 集成小部件 | 编辑
grid.go
,
buttons.go
,
app.go
文件集成小部件 |
grid.go
,
buttons.go
,
app.go
| 见前文对应代码 |
| 构建测试 | 切换到根目录,使用
go build
构建应用 | - |
$ cd ..; go build
|
5. 常见问题与解决方案
在整个开发过程中,可能会遇到一些常见问题,以下是一些可能的问题及解决方案:
5.1 测试时存储库选择问题
- 问题描述 :测试结果不显示使用的存储库后端,不确定是否使用了 SQLite 存储库。
-
解决方案
:
-
监控临时目录,验证是否创建了符合
pomo211866403模式的临时数据库文件。 -
在
getRepo函数中使用t.Log方法打印消息,如Using SQLite repository,以提供可视化反馈。
-
监控临时目录,验证是否创建了符合
5.2 数据库文件未创建问题
-
问题描述
:执行应用后,
pomo.db文件未创建。 -
解决方案
:
-
检查
root.go文件中命令行参数和viper绑定是否正确。 - 确保应用有足够的权限在指定目录创建文件。
-
检查
5.3 小部件显示空白问题
- 问题描述 :运行应用后,每日摘要和每周摘要小部件显示空白。
-
解决方案
:
- 检查是否有历史记录,若没有可运行几个时间段以生成数据。
-
检查
summary.go文件中生成小部件数据的函数是否正确。 -
检查
summaryWidgets.go文件中更新小部件的函数是否正确。
6. 未来优化方向
虽然我们已经完成了番茄钟应用的主要功能,但仍有一些可以优化的方向:
6.1 数据可视化优化
-
可以进一步美化
BarChart和LineChart小部件的样式,如调整颜色、字体、图表布局等,提高用户体验。 - 增加更多的可视化方式,如饼图、柱状图等,以展示不同维度的数据。
6.2 数据统计功能扩展
- 除了每日和每周摘要,还可以添加每月、每年的摘要信息,让用户更全面地了解自己的工作和休息情况。
- 提供更详细的统计信息,如不同时间段的番茄钟使用频率、休息时间分布等。
6.3 性能优化
- 对于数据库查询,优化 SQL 语句,减少查询时间,提高应用的响应速度。
- 对于小部件的更新,采用异步更新的方式,避免阻塞主线程。
通过以上的优化和扩展,可以让番茄钟应用更加完善,为用户提供更好的服务。
综上所述,通过使用 SQLite 存储库、交互式小部件和 SQL 查询功能,我们成功构建了一个功能强大、数据可视化的命令行番茄钟应用,并且可以根据实际需求进行进一步的优化和扩展。
超级会员免费看
61

被折叠的 条评论
为什么被折叠?



