web-dev-qa-db-ja.com

Golangでstructに1対多の多対多のデー​​タベースを効率的にマッピングする

質問

Golangで1対多または多対多のSQL関係を扱う場合、行を構造体にマッピングする最良の(効率的で推奨される「Goのような」)方法は何ですか?

以下の設定例を使用して、私はそれぞれの長所と短所を使用していくつかのアプローチを詳しく説明しようとしましたが、コミュニティが何を推奨しているか疑問に思いました。

必要条件

  • PostgreSQLで動作します(一般的なものでもかまいませんが、MySQL/Oracle固有の機能は含まれません)
  • 効率性-すべての組み合わせを無理矢理強制することはありません
  • ORMなし-理想的にはdatabase/sqljmoiron/sqlxのみを使用します

明確にするために、エラー処理を削除しました

モデル

type Tag struct {
  ID int
  Name string
}

type Item struct {
  ID int
  Tags []Tag
}

データベース

CREATE TABLE item (
  id                      INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
);

CREATE TABLE tag (
  id                      INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  name                    VARCHAR(160),
  item_id                 INT REFERENCES item(id)
);

アプローチ1-すべてのアイテムを選択してから、アイテムごとにタグを選択

var items []Item
sqlxdb.Select(&items, "SELECT * FROM item")

for i, item := range items {
  var tags []Tag
  sqlxdb.Select(&tags, "SELECT * FROM tag WHERE item_id = $1", item.ID)
  items[i].Tags = tags
}

長所

  • シンプルな
  • わかりやすい

短所

  • データベースクエリの数がアイテムの数に比例して増加するため、非効率的

アプローチ2-SQL結合を作成し、行を手動でループする

var itemTags = make(map[int][]Tag)

var items = []Item{}
rows, _ := sqlxdb.Queryx("SELECT i.id, t.id, t.name FROM item AS i JOIN tag AS t ON t.item_id = i.id")
for rows.Next() {
  var (
    itemID  int
    tagID   int
    tagName string
  )
  rows.Scan(&itemID, &tagID, &tagName)
  if tags, ok := itemTags[itemID]; ok {
    itemTags[itemID] = append(tags, Tag{ID: tagID, Name: tagName,})
  } else {
    itemTags[itemID] = []Tag{Tag{ID: tagID, Name: tagName,}}
  }
}
for itemID, tags := range itemTags {
  items = append(Item{
    ID: itemID,
    Tags: tags,
  })
}

長所

  • 大量のメモリを消費することなくループできる単一のデータベース呼び出しとカーソル

短所

  • 構造体に複数の結合と多くの属性があるため、複雑で開発が困難
  • パフォーマンスが高すぎない;メモリ使用量と処理時間の増加とネットワークコールの増加

失敗したアプローチ3-sqlx構造体スキャン

失敗にもかかわらず、私はこのアプローチを、開発の単純さと組み合わせた効率の私の現在の目標であると思うので、含めたいと思います。私の希望は、各構造体フィールドにdbタグを明示的に設定することでしたsqlxは、高度な構造体スキャンを実行できました

var items []Item
sqlxdb.Select(&items, "SELECT i.id AS item_id, t.id AS tag_id, t.name AS tag_name FROM item AS i JOIN tag AS t ON t.item_id = i.id")

残念ながら、これはmissing destination name tag_id in *[]Itemとしてエラーになり、StructScanは行を再帰的にループするほど高度ではないと信じるようになります(批判なし-複雑なシナリオです)

可能なアプローチ4-PostgreSQL配列アグリゲーターおよびGROUP BY

私はこれが動作しないと確信していますが私はそれが改善できるかどうかを確認するためにこのテストされていないオプションを含めましたmay動作します。

var items = []Item{}
sqlxdb.Select(&items, "SELECT i.id as item_id, array_agg(t.*) as tags FROM item AS i JOIN tag AS t ON t.item_id = i.id GROUP BY i.id")

時間があれば、ここでいくつかの実験を行います。

14
Ewan

postgresのSQL:

create schema temp;
set search_path = temp;
create table item
(
  id INT generated by default as identity primary key
);

create table tag
(
  id      INT generated by default as identity primary key,
  name    VARCHAR(160),
  item_id INT references item (id)
);

create view item_tags as
select id,
  (
          select
            array_to_json(array_agg(row_to_json(taglist.*))) as array_to_json
          from (
                select tag.name, tag.id
                 from tag
                         where item_id = item.id
               ) taglist ) as tags
from item ;


-- golang query this maybe 
select  row_to_json(row)
from (
    select * from item_tags
) row;

次に、golangはこのSQLをクエリします:

select  row_to_json(row)
from (
    select * from item_tags
) row;

構造化するために非整列化します。

プロ:

  1. postgresはデータの関係を管理します。 SQL関数を使用してデータを追加/更新します。

  2. golangはビジネスモデルとロジックを管理します。

簡単な方法です。

6
Tsingson Qin

以前使用した別のアプローチを提案できます。

この場合、クエリでタグのjsonを作成して返します。

長所:データを集計するdbへの呼び出しが1つあり、jsonを解析して配列に変換するだけです。

短所:少し見苦しいです。それを私に打ち明けてください。

type jointItem struct {
  Item 
  ParsedTags string
  Tags []Tag `gorm:"-"`
}

var jointItems []*jointItem
db.Raw(`SELECT 
  items.*, 
  (SELECT CONCAT(
            '[', 
             GROUP_CONCAT(
                  JSON_OBJECT('id', id,
                             'name', name 
                  )
             ), 
            ']'
         )) as parsed_tags 
   FROM items`).Scan(&jointItems)

for _, o := range jointItems {
var tempTags []Tag
   if err := json.Unmarshall(o.ParsedTags, &tempTags) ; err != nil {
      // do something
   }
  o.Tags = tempTags
}


編集:コードが奇妙に動作する可能性があるため、同じ構造体を使用するのではなく、移動するときに一時タグ配列を使用する方が良いと思います。

4
Muhamed Keta