30、使用 SQLite 优化番茄钟应用:测试、更新与数据展示

使用 SQLite 优化番茄钟应用:测试、更新与数据展示

1. 用 SQLite 测试存储库

在之前对番茄钟功能进行测试时,我们使用辅助函数 getRepo 来获取存储库,当时只有内存存储库可用。现在添加了 SQLite3 存储库,我们需要提供该函数的另一个版本,以便返回新的存储库。我们可以通过应用构建标签来控制何时使用哪个存储库。

操作步骤如下:
1. 切换到 pomodoro 包目录,创建一个名为 sqlite3_test.go 的文件:

$ cd $HOME/pragprog.com/rggo/persistentDataSQL/pomo/pomodoro
  1. 打开 sqlite3_test.go 文件,添加构建标签 +build !inmemory ,并添加包定义:
//+build !inmemory

package pomodoro_test
  1. 添加导入部分:
import (
    "io/ioutil"
    "os"
    "testing"
    "pragprog.com/rggo/interactiveTools/pomo/pomodoro"
    "pragprog.com/rggo/interactiveTools/pomo/pomodoro/repository"
)
  1. 定义 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())
    }
}
  1. 保存并关闭文件,再次执行测试以测试新的存储库:
$ 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
  1. 编辑 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"))
}
  1. 创建一个新文件 reposqlite.go ,添加构建标签 +build !inmemory 和包定义:
// +build !inmemory

package cmd
  1. 添加导入部分:
import (
    "github.com/spf13/viper"
    "pragprog.com/rggo/interactiveTools/pomo/pomodoro"
    "pragprog.com/rggo/interactiveTools/pomo/pomodoro/repository"
)
  1. 定义 getRepo 函数,根据配置的数据库文件名返回存储库实例:
func getRepo() (pomodoro.Repository, error) {
    repo, err := repository.NewSQLite3Repo(viper.GetString("db"))
    if err != nil {
        return nil, err
    }

    return repo, nil
}
  1. 保存文件,切换回应用的根目录,构建应用以使用新的存储库进行测试:
$ cd ..
$ go build
  1. 运行应用并使用 --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)
}
  1. 实现新方法:
    • 内存存储库 :打开 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
}
  1. 添加生成小部件所需数据的函数:
    • 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
}
  1. 更新应用界面以显示新的小部件:
    • 切换到 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
}
  1. 集成小部件到应用中:
    • 编辑 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)
  1. 切换回应用的根目录,构建新应用进行测试:
$ 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 查询功能,我们成功构建了一个功能强大、数据可视化的命令行番茄钟应用,并且可以根据实际需求进行进一步的优化和扩展。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值