8736 sujets

Développement web côté serveur, CMS

Bonsoir,

Ma question du jour : est-il courant d'avoir plusieurs requêtes SQL pour une page web ? Dans l'idéal j'essaie de regrouper les requêtes mais je n'y arrive pas toujours (cf. le post suivant comme exemple).

Je tente de construire des requêtes SQL pour mon petit CMS maison (sous Postgres). Ça avance lentement (vu le peu de temps libre dont je dispose) mais sûrement. J'apprends pleins de trucs mais mon problème principal est que je reste un amateur et que, en tant que tel, je manque de feedback...

Modifié par Olivier C (17 Apr 2024 - 13:22)
*** Par exemple, voici le script backend pour une page établissant une liste de tous les articles liés à un mot clef (sous WordPress ce serait le template tag.php) pour laquelle j'ai dû faire deux requêtes (pour une bricole en plus, la deuxième requête c'est juste pour récupérer le nom du tag) ; c'est sous Fastify, mais c'est presque comme Express.js :
import { dataConversion } from '../utils/dataConversion.js'

async function modifyData(req, data) {
  data.url = req.url
  data.name = `${data.tagName} <span>.&nbsp;Étiquette</span>`
  data.title = data.name.concat(' . ', process.env.SITE_NAME).replace(/(<([^>]+)>)/gi, '').replace(/&nbsp;/gi, ' ')
  data.description = `Article liés à l'étiquette &quot;${data.tagName}&quot; pour le site ${process.env.SITE_NAME}`
}

export async function getTag(req, res) {
  const client = await this.pg.connect()
  const query = `
    SELECT
      a._id,
      a._name,
      a._alternative_headline,
      a._slug,
      a._created_at,
      a._modified_at,
      a._type,
      a._description,
      a._author_id,
      a._status,
      a._is_restricted_read,
      jsonb_build_object(
        '_username', ac._username,
        '_display_name', ac._display_name
      ) AS _account,
      jsonb_build_object(
        '_given_name', p._given_name,
        '_additional_name', p._additional_name,
        '_family_name', p._family_name,
        '_usual_name', p._usual_name,
        '_nickname', p._nickname,
        '_prefix', p._prefix,
        '_suffix', p._suffix,
        '_url', p._url
      ) AS _author
    FROM
      __article a
    LEFT JOIN
      __account ac ON a._author_id = ac._id
    LEFT JOIN
      __person p ON ac._person_id = p._id
    LEFT JOIN
      __tag_to_article ta ON a._id = ta._article_id
    LEFT JOIN
      __tag t ON ta._tag_id = t._id
    WHERE
      t._slug = $1
    ORDER BY
      a._created_at DESC
    LIMIT
      50
    `
  const query2 = `SELECT _name FROM __tag WHERE _slug = $1`
  let data = {}
  try {
    const { rows } = await client.query(query, [req.params.slug])
    const tagName = await client.query(query2, [req.params.slug])
    if (!rows[0] || !tagName.rows[0]) return res.code(404).view('404')
    data.articles = await Promise.all(rows.map(x => dataConversion(x)))
    data.tagName = await dataConversion(tagName.rows[0]._name)
    modifyData(req, data)
    console.log(data)
    //return data
    return res.view('index', { data })
  } finally {
    client.release()
  }
}

Ce qui donne :
{
  articles: [
    {
      id: 2,
      // les items récupérés...
    },
    {
      id: 4,
      // les items récupérés...
    },
    {
      id: 5,
      // les items récupérés...
    }
  ],
  tagName: 'Anthropologie',
  url: '/tag/anthropologie',
  name: 'Anthropologie <span>.&nbsp;Étiquette</span>',
  title: 'Anthropologie . Étiquette . NomDuSite',
  description: "Article liés à l'étiquette &quot;Anthropologie&quot; pour le site NomDuSite"
}

Auparavant j'avais fait plus simple et réussi à concaténer :
export async function getTag(req, res) {
  const client = await this.pg.connect()
  const query = `
    SELECT
      a._id,
      a._name,
      a._alternative_headline,
      a._slug,
      a._created_at,
      a._modified_at,
      a._type,
      a._description,
      a._author_id,
      a._status,
      a._is_restricted_read,
      jsonb_build_object(
        '_username', ac._username,
        '_display_name', ac._display_name
      ) AS _account,
      jsonb_build_object(
        '_given_name', p._given_name,
        '_additional_name', p._additional_name,
        '_family_name', p._family_name,
        '_usual_name', p._usual_name,
        '_nickname', p._nickname,
        '_prefix', p._prefix,
        '_suffix', p._suffix,
        '_url', p._url
      ) AS _author,
      t._name AS _tag_name
    FROM
      __article a
    LEFT JOIN
      __account ac ON a._author_id = ac._id
    LEFT JOIN
      __person p ON ac._person_id = p._id
    LEFT JOIN
      __tag_to_article ta ON a._id = ta._article_id
    LEFT JOIN
      __tag t ON ta._tag_id = t._id
    WHERE
      t._slug = $1
    ORDER BY
      a._created_at DESC
    LIMIT
      50
    ` // @note Pour l'instant le nom du tag recherché est dupliqué dans chaque article retourné @todo À récupérer au niveau supérieur, à la racine du return de la recherche SQL.
  let data = {}
  try {
    const { rows } = await client.query(query, [req.params.slug])
    if (!rows[0]) return res.code(404).view('404')
    data.articles = await Promise.all(rows.map(x => dataConversion(x)))
    modifyData(req, data)
    console.log(data)
    //return data
    return res.view('index', { data })
  } finally {
    client.release()
  }
}

Le problème dans ce cas, c'est que le nom du tag ("_tag_name") se retrouve lié à chaque article récupéré, et donc dupliqué d'autant. Ce qui rendait la réponse moins propre :
{
  articles: [
    {
      id: 2,
      // les items récupérés...
      tagName: 'Anthropologie'
    },
    {
      id: 4,
      // les items récupérés...
      tagName: 'Anthropologie'
    },
    {
      id: 5,
      // les items récupérés...
      tagName: 'Anthropologie'
    }
  ],
  url: '/tag/anthropologie',
  name: 'Anthropologie <span>.&nbsp;Étiquette</span>',
  title: 'Anthropologie . Étiquette . NomDuSite',
  description: "Article liés à l'étiquette &quot;Anthropologie&quot; pour le site NomDuSite"
}

Modifié par Olivier C (17 Apr 2024 - 01:08)
Après avoir essayé de limiter le nombre de requête au minimum, je me suis rendu compte que
1) ça conduisait à des requêtes très complexes avec un code php très lourd pour retrouver les informations et les réorganiser selon la logique de la page à produire
2) que dans beaucoup de cas c'était impossible en pratique de réduire le nombre de requêtes en dessous d'un 3 ou 4
J'ai donc décidé de donner priorité à la logique des traitements et de ne plus me soucier du nombre de requêtes, quitte à fusionner plusieurs requêtes quand je suis arrivé à faire ce que je veux.
Je suis du même avis que PapyJP

Tant pis j'ai 2, 3 ou plus de requêtes pour une page web... je préfère avoir un code facilement debuggable et factoriser que tout dans une grosse requêtes complexe.
Merci à vous pour vos retours respectifs.
Si d'autres alsanautes veulent donner leur avis je suis preneur.
Les avis de JENCAL et PapyJP sont dans le vrai !
A moins que tu ne prévois une fréquentation exponentielle, le plus simple, même si cela implique ++ de requêtes est préféré.

Il s'agit vraiment de cibler vers qui s'adresse la solution ? Des particuliers ? Des compagnies web ?
Merci pour votre réponse Gordon25,

Pour votre question : rien de tout cela, c'est du pur amateurisme.

C'est à dire que je suis un peu jusque-boutiste sur les bords, et comme je suis têtu j'ai tout de même persévéré dans le sens de la mono-requête (au pire : 2).

Auparavant je m'étais intéressé aux modélisations de base de données, je ne m'intéresse sérieusement aux requêtes SQL que depuis deux mois. Je n'aime pas les ORMs. En effet, entre se taper la compréhension d'un ORM qui va me limiter/poser des problèmes de perf's dans certains cas, et attaquer directement le SQL, je préfère nettement attaquer bas niveau avec SQL. Je ne trouve pas cela si difficile au final. Je tâtonne un peu bien sûr, codant seul dans mon coin je manque de feedback, d'où ce post...

Pour vous montrer où j'en suis, voici une requête sous Postgres pour GET un article :
SELECT
a.*,
jsonb_build_object(
  '_given_name', p._given_name,
  '_additional_name', p._additional_name,
  '_family_name', p._family_name,
  '_usual_name', p._usual_name,
  '_nickname', p._nickname,
  '_prefix', p._prefix,
  '_suffix', p._suffix,
  '_url', p._url,
  '_name', CONCAT_WS(' ', p._given_name, p._family_name),
  '_full_name', CONCAT_WS(', ', CONCAT_WS(' ', p._prefix, p._given_name, p._additional_name, p._family_name), p._suffix),
  '_media' , (
      SELECT
        jsonb_build_object(
          '_name', m._name,
          '_type', m._type,
          '_url', m._url,
          '_description', m._description
        )
      FROM __media m
      WHERE m._id = ac._media_id
    ),
  '_username', ac._username,
  '_display_name', ac._display_name
) AS _author,
(
  SELECT jsonb_agg(
    jsonb_build_object(
      '_id', c._id,
      '_content', c._content,
      '_created_at', c._created_at,
      '_modified_at', c._modified_at,
      '_commentator_id', c._account_id,
      '_author' , jsonb_build_object(
        '_display_name', ac._display_name,
        '_username', ac._username,
        '_given_name', p._given_name,
        '_additional_name', p._additional_name,
        '_family_name', p._family_name,
        '_usual_name', p._usual_name,
        '_nickname', p._nickname,
        '_prefix', p._prefix,
        '_suffix', p._suffix,
        '_media_id', p._media_id,
        '_name', CONCAT_WS(' ', p._given_name, p._family_name),
        '_full_name', CONCAT_WS(', ', CONCAT_WS(' ', p._prefix, p._given_name, p._additional_name, p._family_name), p._suffix),
        '_media', (
            SELECT jsonb_build_object(
              '_name', m._name,
              '_type', m._type,
              '_url', m._url,
              '_description', m._description
            )
            FROM __media m
            WHERE m._id = ac._media_id
        )
      )
    )
    ORDER BY c._id
  )
  FROM __comment c
  LEFT JOIN __account ac ON c._account_id = ac._id
  LEFT JOIN __person p ON c._account_id = p._id
  WHERE c._article_id = a._id
) AS _comments,
(
  SELECT jsonb_agg(
    jsonb_build_object(
      '_name', t._name,
      '_slug', t._slug
    )
  )
  FROM __tag_to_article ta
  LEFT JOIN __tag t ON ta._tag_id = t._id
  WHERE ta._article_id = a._id
) AS _tags,
(
  SELECT jsonb_agg(
    jsonb_build_object(
      '_name', m._name,
      '_type', m._type,
      '_url', m._url,
      '_description', m._description
    )
  )
  FROM __media_to_article ma
  LEFT JOIN __media m ON ma._media_id = m._id
  WHERE ma._article_id = a._id
) AS medias
FROM
__article a
LEFT JOIN
__account ac ON a._author_id = ac._id
LEFT JOIN
__person p ON ac._person_id = p._id
WHERE
a._slug = 'le-slug-de-article'
GROUP BY
a._id, p._id, ac._id;

Ce qui donnera cette réponse à exploiter (après traitement des clefs, des dates et autres bricoles via JS) :
{
  "id": 5,
  "name": "Agent 327",
  "alternativeHeadline": "",
  "slug": "agent-327",
  "content": "Un contenu...",
  "createdAt": "2022-04-16T17:10:25.000Z",
  "modifiedAt": "2022-04-16T18:15:22.000Z",
  "type": null,
  "description": "Test pour une vidéo...",
  "authorId": 4,
  "status": 1,
  "isReadable": true,
  "isCommentable": true,
  "isVisibleComments": true,
  "add": null,
  "author": {
    "url": null,
    "name": "",
    "media": {
      "url": "/medias/images/uploads/charles-de-foucauld-original.webp",
      "name": null,
      "type": null,
      "description": null
    },
    "prefix": null,
    "suffix": null,
    "nickname": null,
    "username": "Iota",
    "fullName": "",
    "givenName": null,
    "usualName": null,
    "familyName": null,
    "displayName": "username",
    "additionalName": null
  },
  "comments": [
    {
      "id": 1,
      "author": {
        "name": "Jeanne d'Arc",
        "media": {
          "url": "/medias/images/uploads/jeanne-d-arc-original.webp",
          "name": null,
          "type": null,
          "description": null
        },
        "prefix": "Ste",
        "suffix": null,
        "mediaId": 2,
        "nickname": "La Pucelle d'Orléans",
        "username": "Beta",
        "fullName": "Ste Jeanne d'Arc",
        "givenName": "Jeanne",
        "usualName": null,
        "familyName": "d'Arc",
        "displayName": "nickname",
        "additionalName": null
      },
      "content": "Un petit commentaire. Un.",
      "createdAt": "2022-04-16T19:10:25",
      "modifiedAt": "2022-04-16T19:17:22",
      "commentatorId": 2
    },
    {
      "id": 2,
      "author": {
        "name": "Charles de Gaulle",
        "media": {
          "url": "/medias/images/uploads/charles-de-gaulle-original.webp",
          "name": null,
          "type": null,
          "description": null
        },
        "prefix": "Général",
        "suffix": null,
        "mediaId": 3,
        "nickname": "Le Général",
        "username": "Delta",
        "fullName": "Général Charles André Joseph Marie de Gaulle",
        "givenName": "Charles",
        "usualName": null,
        "familyName": "de Gaulle",
        "displayName": "name",
        "additionalName": "André Joseph Marie"
      },
      "content": "Un petit commentaire. Deux.",
      "createdAt": "2022-04-16T19:15:25",
      "modifiedAt": "2022-04-16T20:25:22",
      "commentatorId": 3
    },
    {
      "id": 3,
      "author": {
        "name": "",
        "media": {
          "url": "/medias/images/uploads/charles-de-foucauld-original.webp",
          "name": null,
          "type": null,
          "description": null
        },
        "prefix": null,
        "suffix": null,
        "mediaId": null,
        "nickname": "El Comandante",
        "username": "Iota",
        "fullName": "",
        "givenName": null,
        "usualName": null,
        "familyName": null,
        "displayName": "username",
        "additionalName": null
      },
      "content": "Un petit commentaire. Trois.",
      "createdAt": "2022-04-16T21:35:25",
      "modifiedAt": "2022-04-16T22:15:22",
      "commentatorId": 4
    },
    {
      "id": 4,
      "author": {
        "name": "Henri de Lubac",
        "media": {
          "url": "/medias/images/uploads/henri-de-lubac-original.webp",
          "name": null,
          "type": null,
          "description": null
        },
        "prefix": "P.",
        "suffix": "s.j.",
        "mediaId": 1,
        "nickname": null,
        "username": "Alpha",
        "fullName": "P. Henri Sonier de Lubac, s.j.",
        "givenName": "Henri",
        "usualName": null,
        "familyName": "de Lubac",
        "displayName": "fullName",
        "additionalName": "Sonier"
      },
      "content": "Un petit commentaire. Quatre.",
      "createdAt": "2022-04-17T10:45:25",
      "modifiedAt": "2022-04-17T10:47:12",
      "commentatorId": 1
    },
    {
      "id": 5,
      "author": {
        "name": "Jeanne d'Arc",
        "media": {
          "url": "/medias/images/uploads/jeanne-d-arc-original.webp",
          "name": null,
          "type": null,
          "description": null
        },
        "prefix": "Ste",
        "suffix": null,
        "mediaId": 2,
        "nickname": "La Pucelle d'Orléans",
        "username": "Beta",
        "fullName": "Ste Jeanne d'Arc",
        "givenName": "Jeanne",
        "usualName": null,
        "familyName": "d'Arc",
        "displayName": "nickname",
        "additionalName": null
      },
      "content": "Un petit commentaire. Cinq.",
      "createdAt": "2022-05-02T09:10:25",
      "modifiedAt": "2022-05-02T10:07:22",
      "commentatorId": 2
    }
  ],
  "tags": [
    {
      "name": "Anthropologie",
      "slug": "anthropologie"
    },
    {
      "name": "Paroles des Pères",
      "slug": "paroles-des-peres"
    },
    {
      "name": "Ressources en ligne",
      "slug": "ressources-en-ligne"
    }
  ],
  "medias": null
}

Lorsque le projet sera stable, je convertirai peut-être tout cela en fonction stockée.
Modifié par Olivier C (04 May 2024 - 15:40)
c'est propre ce que tu fais hein Smiley smile mais cette requête (là tu es seul a va) mais en équipe, le jour où le mec après toi dois la debugger... bonjour le casse tête.

Moi je réfléchie (j'essaye) toujours sur la base d'une question : Est ce que le dev après moi va pas trop se prendre la tête, et du coup je préfère opter pour la méthode KISS

Si je dois débugger (ou simplement juste la comprendre) une requête qui n'est pas la mienne et qui ressemble à la tienne, je prend peur et je sais que je vais passer bcp de temps.
Les requêtes en soit prennent des micros secondes alors bon... est ce impacté d'en avoir plusieurs je pense pas. Ce qui ralenti, c'est l'algo derrière.
Modifié par JENCAL (07 May 2024 - 09:43)
Bonjour,

D'après mon expérience le nombre de requêtes peut largement impacter le temps d'affichage et cela indépendamment de l'algo derrière.

Une requête pour sélectionner toutes les commandes d'un client, disons 100.
Pour chaque commande, une requête très simple pour avoir le numéro de facture, donc 100 requêtes très simples.
Là tu vois le temps passer, quelques secondes...

Une requête un peu plus compliquée (mais sans JOIN) pour sélectionner toutes les commandes ET numéros de factures d'un client.
Affichage quasi-instantanée.

C'est mon expérience sur un cas réel, je ne suis pas expert en SQL.