🔥

Firestoreドキュメントとパス情報を別々に管理する

2023/02/04に公開

はじめに

本記事は前記事で紹介したドキュメントにパス情報を含めた場合の実装に対し、パス情報をドキュメントとは別に定義する場合の実装について記載してみます。

想定するデータモデル

前回と変わりません。

  • データへのパス
/users/{{uid}}/books/{{bookId}}
  • データ
{
  title:        "xxxxxx",  // 本のタイトル
  description:  "xxxxxx",  // 本の内容
}

Go言語での実装について

前回同様、サーバサイドの実装を書いていきます。

// ユーザが所持する本
type UsersBook struct {
	Title       string `firestore:"title"`
	Description string `firestore:"description"`
	// その他のデータが続く
}

// UsersBookへのパス情報
type UsersBookPath struct {
	UID    string `firestore:"uid"`
	BookID string `firestore:"bookId"`
}

firestoreへのSet/Get関数はパス情報とデータを格納する変数の2つを渡します。

func (ub UsersBookPath) Path() string {
	return fmt.Sprintf("users/%s/books/%s", ub.UID, ub.BookID)
}

func SetWithPath(ctx context.Context, f *firestore.Client, path Pathable, data interface{}) error {
	_, err := f.Doc(path.Path()).Set(ctx, data)
	return err
}

func GetWithPath(ctx context.Context, f *firestore.Client, path Pathable, data interface{}) error {
	snapshot, err := f.Doc(path.Path()).Get(ctx)
	if err != nil {
		return err
	}
	return snapshot.DataTo(data)
}

サンプルコード

データを1件読み込んで、編集後に書き込むサンプルコードです。
前回と比べても変数が増えたのみで大きくは変わりません。

func readWrite(uid, bookID string) {
	// パス情報を作成する
	path := UsersBookPath{
		UID:    uid,
		BookID: bookID,
	}

	// データ格納用変数
	book := UsersBook{}
	// データ読み込み
	_ = GetWithPath(ctx, f, path, &book)

	book.Title = "xxxx"

	// データ書き込み
	_ = SetWithPath(ctx, f, path, &book)
}

クエリで一覧を取得する

クエリでデータの一覧を取得しますが、そのデータを再度firestoreに書き込む場合に備え、パス情報も同時に一覧化する必要があります。
そのためにいくつか工夫が必要になってきます。まず、クエリ結果の一つであるドキュメント参照情報(firestore.DocumentRef)からパス情報を作るためのインターフェースを定義します。

type Unpathable interface {
	Unpath(ref *firestore.DocumentRef)
}

func (p *UsersBookPath) Unpath(ref *firestore.DocumentRef) {
	p.BookID = ref.ID
	p.UID = ref.Parent.Parent.ID
}

上記のインターフェースを用いてクエリを元にfirestoreからデータとパス情報の一覧を取得します。

func ListWithPath[T any, P comparable, PP interface {
	Unpathable
	*P
}](ctx context.Context, q firestore.Query) (map[P]*T, []P, error) {
	mapList := make(map[P]*T)
	paths := []P{}
	iter := q.Documents(ctx)
	snapshots, err := iter.GetAll()
	if err != nil {
		return mapList, paths, err
	}
	for _, snapshot := range snapshots {
		var data T
		path := PP(new(P))
		path.Unpath(snapshot.Ref)
		if err = snapshot.DataTo(&data); err != nil {
			return mapList, paths, err
		}
		mapList[*path] = &data
		paths = append(paths, *path)
	}
	return mapList, paths, nil
}

func searchBooksAndUpdate(title string) {
	// ... 前処理
	q := f.CollectionGroup("books").Where("title", "==", title)
	books, paths, _ := ListWithPath[UsersBook, UsersBookPath](ctx, q)
	for _, path := range paths {
		// book取り出し
		book := books[path]
		// ... book 更新処理
		_ = SetWithPath(ctx, f, &path, &book)
	}
}

ListWithPathでクエリ結果としてデータとパス情報の一覧を取得できるようにしていますが、データの一覧は配列ではなくパス情報をキーとしたマップになっています。

クエリで得られるデータの一覧をわざわざマップにしている理由については、次回のfirestoreでJOINを実現する実装で解説したいと思います。

Discussion