web-dev-qa-db-fra.com

Mappage efficace d'une base de données un-à-plusieurs-à-plusieurs à une structure dans Golang

Question

Lorsqu'il s'agit d'une relation SQL un-à-plusieurs ou plusieurs-à-plusieurs dans Golang, quelle est la meilleure façon (efficace, recommandée, "Go-like") de mapper les lignes à une structure?

En prenant l'exemple de configuration ci-dessous, j'ai essayé de détailler certaines approches avec les avantages et les inconvénients de chacun, mais je me demandais ce que la communauté recommande.

Exigences

  • Fonctionne avec PostgreSQL (peut être générique mais ne pas inclure de fonctionnalités spécifiques à MySQL/Oracle)
  • Efficacité - Aucune brute ne force toutes les combinaisons
  • Pas d'ORM - Idéalement en utilisant uniquement database/sql et jmoiron/sqlx

Exemple

Par souci de clarté, j'ai supprimé la gestion des erreurs

Modèles

type Tag struct {
  ID int
  Name string
}

type Item struct {
  ID int
  Tags []Tag
}

Base de données

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)
);

Approche 1 - Sélectionnez tous les articles, puis sélectionnez des étiquettes par article

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
}

Avantages

  • Facile
  • Facile à comprendre

Inconvénients

  • Inefficace avec le nombre de requêtes de base de données augmentant proportionnellement au nombre d'éléments

Approche 2 - Construire la jointure SQL et parcourir les lignes manuellement

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,
  })
}

Avantages

  • Un seul appel de base de données et un curseur qui peuvent être bouclés sans manger trop de mémoire

Inconvénients

  • Compliqué et plus difficile à développer avec plusieurs jointures et de nombreux attributs sur la structure
  • Pas trop performant; plus de mémoire et de temps de traitement par rapport à plus d'appels réseau

Échec de l'approche 3 - analyse de la structure sqlx

Malgré l'échec, je veux inclure cette approche car je trouve que c'est mon objectif actuel d'efficacité associé à la simplicité de développement. Mon espoir était de définir explicitement la balise db sur chaque champ struct sqlx pourrait faire un scan de structure avancé

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")

Malheureusement, cela se traduit par missing destination name tag_id in *[]Item ce qui me fait croire que le StructScan n'est pas assez avancé pour parcourir récursivement les lignes (pas de critique - c'est un scénario compliqué)

Approche possible 4 - Agrégateurs de tableaux PostgreSQL et GROUP BY

Bien que je sois sûr que cela ne fonctionnera pas , j'ai inclus cette option non testée pour voir si elle pourrait être améliorée afin qu'elle peut fonctionner.

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")

Quand j'aurai du temps, j'essaierai de faire quelques expériences ici.

14
Ewan

le sql en postgres:

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;

puis golang interroge ce sql:

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

et démasquer pour aller struct:

pro:

  1. postgres gère la relation des données. ajouter/mettre à jour des données avec les fonctions sql.

  2. golang gère le modèle commercial et la logique.

c'est facile.

.

6
Tsingson Qin

Je peux suggérer une autre approche que j'ai utilisée auparavant.

Vous créez un json des balises dans ce cas dans la requête et le renvoyez.

Avantages: Vous avez 1 appel à la base de données, qui agrège les données, et tout ce que vous avez à faire est d'analyser le json dans un tableau.

Inconvénients: C'est un peu moche. N'hésitez pas à me critiquer pour cela.

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
}


Edit: le code peut se comporter bizarrement, donc je trouve préférable d'utiliser un tableau de balises temporaires lors du déplacement au lieu d'utiliser la même structure.

4
Muhamed Keta