是否需要额外往返Firestore以读取创建/更新时间戳字段?
我用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

