在Firestore本地的Go中编组/解除定制结构。
#编程 #go #googlecloud #firestore

动机

Firestore本地,基于文档的接口比数据存储模式在使用层次数据时更容易使用。那是您的数据不是基于表/记录的,而是类似的图。但是它不像数据存储模式那样支持自定义填写。但是我已经实施了一项工作。

解决方案

我添加了MarshalMapUnmarshalMap函数,这些功能模仿了MarshalJsonUnmarshalJson接口支持的合同。它们可能是接口,但这将需要很多重复的代码,因为所有实现都将与每种类型的这两个函数完全相同。

而不是返回和消费[]byte,而是在实践中返回和消耗map[string]interface{},或者从技术上讲map[string]stringmap[string]interface{}

它的工作方式是我将我的自定义结构合并到JSON,然后将它们元使用到map[string]interface{},但是由于JSON中的所有字段都是string类型,因此最终是map[string]string

在API中支持类似的东西应该很微不足道。

我知道我的解决方案是一种na -ve解决方案,在计算机上的时间或空间效率不高,但是编写代码时效率很高。

这使我对文档中数据的表示方式以及如何将其转换回JSON的类型安全结构。

使每个值字段成为string,甚至numbertimestamp也使我对数据的控制权更大,并在调试时更容易在控制台中阅读。布尔类型是我唯一不需要成为string的东西,但我通常将它们存储为string,无论如何都以保持一致性和语义。 yes/noon/offenabled/disabled等总是比语义上的true/false更明确和信息。

这只是其中的第一个版本,当我启动应用程序时,我可以尝试根据需要尝试优化现实世界的数据集和指标。

这是代码

导入以下代码段,我讨厌当人们忽略进口并让您猜测他们实际使用的模块时。

import (
    "encoding/json"
    "github.com/rs/zerolog/log"
)

这只是json.Marshal()周围的Must包装器,因为我想确保所有数据在运行时都正确。是的,我爱Erlang,恨我,如果您愿意,这可以起作用!

func MustMarshalJson(o any) []byte {
    bytes, err := json.Marshal(o)
    if err != nil {
        log.Fatal().Err(err).Msg(err.Error())
        return []byte{}
    }
    return bytes
}

这只是json.Unmarshal()周围的Must包装器,因为我想确保所有数据在运行时都正确。是的,我爱Erlang,恨我,如果您愿意,这可以起作用!

func MustUnMarshalJson(bytes []byte, o any) {
    err := json.Unmarshal(bytes, o)
    if err != nil {
        log.Fatal().Err(err).Msg(err.Error())
        return
    }
    return
}

这是一个具有适当json:""标签或自定义MarshalJson/UnmarshalJson接口实现的结构,然后将其转换为map[string]interface{}
这是隐式的Must风格接口,它起作用或失败并阻止了世界。

func MarshallMap[T any](o T) map[string]interface{} {
    m := make(map[string]interface{})
    MustUnMarshalJson(MustMarshalJson(o), &m)
    return m
}

这是一个地图,将其转换为JSON,然后将JSON转换为结构。
这是隐式的Must风格接口,它起作用或失败并阻止了世界。

func UnmarshallMap[T any](m map[string]interface{}, o T) {
    MustUnMarshalJson(MustMarshalJson(m), o)
}

这就是您的使用方式,我包括了样板客户端的创建,我讨厌当人们抛弃使示例不完整的东西时,我猜测您的设置实际上在做什么。

这是文档的写入。

func Create(ctx context.Context, a *Account) error {
    client, err := firestore.NewClient(ctx, os.Getenv("GOOGLE_CLOUD_PROJECT"))
    if err != nil {
        log.Fatal().Err(err).Msg(err.Error())
    }
    defer func(client *firestore.Client) {
        err := client.Close()
        if err != nil {
            log.Err(err).Msg(err.Error())
        }
    }(client)
    docRef := client.Doc(fmt.Sprintf("account/%s", a.Id))

    m := server.MarshallMap[*Account](a)
    _, err = docRef.Create(ctx, m)
    if err != nil {
        return err
    }
    return nil
}

这是文档中的读取的样子。

func Get(ctx context.Context, id string) (*Account, error) {
    client, err := firestore.NewClient(ctx, os.Getenv("GOOGLE_CLOUD_PROJECT"))
    if err != nil {
        log.Fatal().Err(err).Msg(err.Error())
    }
    defer func(client *firestore.Client) {
        err := client.Close()
        if err != nil {
            log.Err(err).Msg(err.Error())
        }
    }(client)

    docRef := client.Doc(fmt.Sprintf("account/%s", id))
    docSnapshot, err := docRef.Get(ctx)
    if err != nil {
        return nil, err
    }
    a := Account{}
    server.UnmarshallMap[*Account](docSnapshot.Data(), &a) // << this is the magic part
    return &a, nil
}

很棒的事情是,您可以免费元帅/UNMARSHAL。
这是我正在使用的结构定义,这是我正在从事的真实项目中的真实代码。

type Account struct {
    Id            string              `json:"id"`
    Name          string              `json:"name"`
    Email         string              `json:"email"`
    Photo         string              `json:"photo"`
    EmailVerified timestamp.Timestamp `json:"email_verified"`
    Created       timestamp.Timestamp `json:"created"`
    LastUpdated   timestamp.Timestamp `json:"last_updated"`
}

和自定义Timestamp我喜欢用来更好地控制数据的表示。

type Timestamp struct {
    t time.Time
}

func (ts Timestamp) String() string {
    bytes, _ := ts.MarshalText()
    return string(bytes)
}

func (ts Timestamp) MarshalText() (text []byte, err error) {
    return []byte(ts.t.UTC().Format(time.RFC3339Nano)), nil
}

func (ts *Timestamp) UnmarshalText(b []byte) error {
    t, err := time.Parse(time.RFC3339Nano, string(b))
    if err != nil {
        return err
    }
    ts.t = t.UTC()
    return nil
}

func (ts Timestamp) MarshalJSON() ([]byte, error) {
    return ts.MarshalText()
}

func (ts *Timestamp) UnmarshallJSON(b []byte) error {
    return ts.UnmarshalText(b)
}

func (ts Timestamp) MarshalBinary() (data []byte, err error) {
    return ts.MarshalText()
}

func (ts *Timestamp) UnmarshalBinary(b []byte) error {
    return ts.UnmarshalText(b)
}

我最初将time.Time作为匿名作品,但我认为我不希望我的Timestamp隐式转换为time.Time,因为它在其他模块中混淆了序列化。因此,我给了它一个私人名称t,并具有可转换/从time.Time转换/适应的功能,以与其他库的兼容。 Explicit is better than Implicit,我更喜欢python哲学,而不是语言。

func From(t time.Time) Timestamp {
    return Timestamp{
        t: t.UTC(),
    }
}

func To(ts Timestamp) time.Time {
    return ts.t
}

您应该始终在UTC中存储和运输timestamp数据。

所有计算都应在UTC值上完成,仅转换为显示的本地时区。