You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

是否需要额外往返Firestore以读取创建/更新时间戳字段?

Firestore更新Ticket后获取serverTimestamp字段的实现合理性探讨

我用Go开发了一个REST API,基于Firestore Go客户端实现Ticket资源的存储。为了支持按创建/更新时间排序,我按照文档要求将这两个字段存为时间戳,并添加serverTimestamp标签,让字段值取Firestore服务器处理请求的时间。

更新操作的HTTP响应需要包含如下结构:

{
 "ticket": {
   "id": "af41766e-76ea-43b5-86c1-8ba382edd4dc",
   "title": "Ticket updated title",
   "price": 9,
   "date_created": "2023-01-06 09:07:24",
   "date_updated": "2023-01-06 10:08:24"
 }
}

这意味着更新Ticket文档后,除了修改title或price,还得拿到date_updated的最新值。目前功能正常,但我不确定当前实现是否合理——如代码所示,我用事务更新Ticket,但除了重新读取更新后的Ticket,没找到其他获取DateUpdated字段更新值的方法。

领域实体定义

package tixer

import (
    "context"
    "time"

    "github.com/google/uuid"
)

type (
    // TicketID 代表Ticket的唯一标识符,属于领域类型
    TicketID uuid.UUID

    // Ticket 代表系统中的单个Ticket,属于领域类型
    Ticket struct {
        ID          TicketID
        Title       string
        Price       float64
        DateCreated time.Time
        DateUpdated time.Time
    }
)

Firestore交互代码(创建与更新)

// Storer 负责在Firestore中持久化Ticket
type Storer struct {
    client *firestore.Client
}

func NewStorer(client *firestore.Client) *Storer {
    return &Storer{client}
}

func (s *Storer) CreateTicket(ctx context.Context, ticket *tixer.Ticket) error {
    writeRes, err := s.client.Collection("tickets").Doc(ticket.ID.String()).Set(ctx, createTicket{
        Title: ticket.Title,
        Price: ticket.Price,
    })

    // 这里writeRes.UpdateTime就是文档创建的时间
    ticket.DateCreated = writeRes.UpdateTime

    return err
}

func (s *Storer) UpdateTicket(ctx context.Context, ticket *tixer.Ticket) error {
    docRef := s.client.Collection("tickets").Doc(ticket.ID.String())
    err := s.client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
        doc, err := tx.Get(docRef)
        if err != nil {
            switch {
            case status.Code(err) == codes.NotFound:
                return tixer.ErrTicketNotFound
            default:
                return err
            }
        }
        var t persistedTicket
        if err := doc.DataTo(&t); err != nil {
            return err
        }
        t.ID = doc.Ref.ID

        if ticket.Title != "" {
            t.Title = ticket.Title
        }
        if ticket.Price != 0 {
            t.Price = ticket.Price
        }

        return tx.Set(docRef, updateTicket{
            Title:       t.Title,
            Price:       t.Price,
            DateCreated: t.DateCreated,
        })
    })
    if err != nil {
        return err
    }

    updatedTicket, err := s.readTicket(ctx, ticket.ID)
    if err != nil {
        return err
    }
    *ticket = updatedTicket

    return nil
}

func (s *Storer) readTicket(ctx context.Context, id tixer.TicketID) (tixer.Ticket, error) {
    doc, err := s.client.Collection("tickets").Doc(id.String()).Get(ctx)
    if err != nil {
        switch {
        case status.Code(err) == codes.NotFound:
            return tixer.Ticket{}, tixer.ErrTicketNotFound
        default:
            return tixer.Ticket{}, err
        }
    }

    var t persistedTicket
    if err := doc.DataTo(&t); err != nil {
        return tixer.Ticket{}, err
    }
    t.ID = doc.Ref.ID

    return toDomainTicket(t), nil
}

type (
    // persistedTicket 代表Firestore中存储的Ticket结构
    persistedTicket struct {
        ID          string    `firestore:"id"`
        Title       string    `firestore:"title"`
        Price       float64   `firestore:"price"`
        DateCreated time.Time `firestore:"dateCreated"`
        DateUpdated time.Time `firestore:"dateUpdate"`
    }

    // createTicket 包含在Firestore创建Ticket所需的数据
    createTicket struct {
        Title       string    `firestore:"title"`
        Price       float64   `firestore:"price"`
        DateCreated time.Time `firestore:"dateCreated,serverTimestamp"`
        DateUpdated time.Time `firestore:"dateUpdate,serverTimestamp"`
    }
    // updateTicket 包含在Firestore更新Ticket所需的数据
    updateTicket struct {
        Title       string    `firestore:"title"`
        Price       float64   `firestore:"price"`
        DateCreated time.Time `firestore:"dateCreated"`
        DateUpdated time.Time `firestore:"dateUpdate,serverTimestamp"`
    }
)

func toDomainTicket(t persistedTicket) tixer.Ticket {
    return tixer.Ticket{
        ID:          tixer.TicketID(uuid.MustParse(t.ID)),
        Title:       t.Title,
        Price:       t.Price,
        DateCreated: t.DateCreated,
        DateUpdated: t.DateUpdated,
    }
}

实现分析与优化建议

你的当前实现是可行的,但存在一些可以优化的点:

1. 事务后二次读取的合理性

因为serverTimestamp是Firestore服务器生成的,客户端在写操作时无法直接获取该值,所以事务完成后二次读取文档是合理且不可避免的——Firestore的事务和写操作都不会返回服务器生成的时间戳,必须通过读取最新文档来获取。

2. 简化事务逻辑

当前事务中先读取全量文档、修改字段后再用tx.Set写回的方式,可改为tx.Update只更新需要修改的字段,减少数据传输量:

// 修改UpdateTicket中的事务逻辑
return tx.Update(docRef, []firestore.Update{
    {Path: "title", Value: t.Title},
    {Path: "price", Value: t.Price},
    {Path: "dateUpdate", Value: firestore.FieldValue.ServerTimestamp()},
})

这种方式无需传递整个文档结构,更高效,同时保持事务的原子性。

3. 事务的必要性判断

如果你的业务场景不存在并发修改同一Ticket的情况,可以去掉事务,直接用DocRef.Update完成修改,再读取最新文档即可;如果有并发修改需求,事务是必须的,它能保证读取-修改-写入的原子性,避免数据冲突。

总结

你的实现逻辑是合理的,二次读取是获取Firestore服务器生成时间戳的唯一可行方式。若要优化,建议将tx.Set改为tx.Update来减少不必要的数据传输,同时根据业务场景判断是否需要保留事务。

内容的提问来源于stack exchange,提问作者Robert Mihai

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 01:45:44