sunburst
sunburst

Reputation: 103

get 'related' data from several tables with a single query in PHP/MySql?

I'm developing a small PHP app from scratch to practice/learn. The goal is to be able to create articles. Each article can have tags and images, stored in their own tables. Since an article can have many tags and a single tag can be present in many articles, I also created a 'tag/article' association table.

Now, when I want to edit or display an article, I get its data (plus tags and images) by doing several DB requests... for example, given an article id, I do something like:

$article = getArticle($articleId);
$tags = getArticleTags($articleId);
$images = getArticleImages($articleId);

It works, but I'm not sure if it's a clean approach and good performance-wise. So I was wondering, can this be done by using a single query? I tried but got a bit confused with UNIONs and JOINs. If not, is there any better/recommended approach to achieve this?

The (simplified) tables I'm using are:

articles table:

┌-----┬------------------┬------------------------┐
│ id  │ title            │ dateCreated            │
├-----┼------------------┼------------------------┤
│ 1   │ some article     │ 2015-03-17 12:38:49    │
├-----┼------------------┼------------------------┤
│ 2   │ article two      │ 2015-03-17 15:00:24    │
└-----┴------------------┴------------------------┘ 

tags table:

┌-----┬------------------┬------------------------┐
│ id  │ name             │ dateCreated            │
├-----┼------------------┼------------------------┤
│ 1   │ php              │ 2015-03-17 15:01:05    │
├-----┼------------------┼------------------------┤
│ 2   │ jquery           │ 2015-03-17 15:24:12    │
└-----┴------------------┴------------------------┘ 

tag-article association table:

┌-----┬--------┬-------------┬------------------------┐
│ id  │ tagId  │ articleId   │ dateCreated            │
├-----┼--------┼-------------┼------------------------┤
│ 1   │ 1      │ 1           │ 2015-03-17 15:01:06    │
├-----┼--------┼-------------┼------------------------┤
│ 2   │ 2      │ 1           │ 2015-03-17 15:24:58    │
├-----┼--------┼-------------┼------------------------┤
│ 3   │ 1      │ 2           │ 2015-03-17 15:30:38    │
└-----┴--------┴-------------┴------------------------┘

article-images table:

┌-----┬-----------┬-------------┬------------------------┐
│ id  │ fileName  │ articleId   │ dateCreated            │
├-----┼-----------┼-------------┼------------------------┤
│ 1   │ pic1.jpg  │ 1           │ 2015-03-17 16:05:26    │
├-----┼-----------┼-------------┼------------------------┤
│ 2   │ pic2.jpg  │ 1           │ 2015-03-17 16:06:29    │
├-----┼-----------┼-------------┼------------------------┤
│ 3   │ dog.jpg   │ 2           │ 2015-03-17 17:00:14    │
├-----┼-----------┼-------------┼------------------------┤
│ 4   │ cat.jpg   │ 2           │ 2015-03-17 17:01:06    │
├-----┼-----------┼-------------┼------------------------┤
│ 5   │ bird.jpg  │ 2           │ 2015-03-17 17:02:34    │
└-----┴-----------┴-------------┴------------------------┘

Also, I'm using PDO this way, for example, to get an article:

    public function getArticleById($id){
        $sql = "SELECT * FROM articles WHERE id=:id LIMIT 1";
        $query = $this->db->prepare($sql);
        $parameters = array(':id' => $id);
        $result = $query->execute($parameters);
        return ($result) ? $query->fetch() : 0;
    }

Any input will be greatly appreciated. Thanks!


UPDATE:

@Adrian's answer is close to what I'm looking for and partially solves the question. It returns the following enter image description here

The thing is that the id columns are ambiguous and I can't figure out how to end up with a clean array like this, ideally:

Array
(
    [title] => some article
    [id] => 1
    [tags] => Array
        (
            [0] => Array
                (
                    [id] => 1
                    [name] => php
                )

            [1] => Array
                (
                    [id] => 2
                    [name] => jquery
                )

        )

    [images] => Array
        (
            [0] => Array
                (
                    [id] => 1
                    [fileName] => pic1.jpg
                )

            [1] => Array
                (
                    [id] => 2
                    [fileName] => pic2.jpg
                )

        )

)

Then, combining both Daniel's and Adrian's answers, I ended up with this query, which returns something more readable that (I guess) I could process to achieve the above mentioned array (I imagine that iterating and removing duplicates):

SELECT 
    a.id, 
    a.title, 
    a.dateCreated as articleDate,
    i.id as imageId,
    i.fileName as imageFile,
    t.id as tagId,
    t.name as tagName
FROM 
    articles a
JOIN 
    article_tags ta ON a.id = ta.articleId
JOIN
    tags t ON ta.tagId = t.id

JOIN 
    images i ON a.id = i.articleId


WHERE 
    a.id = 1

This returns:

enter image description here

performance-wise, since this possible solution returns many rows with duplicated data (depending on how many tags and images are associated with an article). Is this better than doing the initial multiple calls I started with?

Upvotes: 0

Views: 227

Answers (3)

sunburst
sunburst

Reputation: 103

I've found a way to achieve what I wanted, by combining the answers posted by @Adrian and @Daniel who pointed me in the right direction (thanks!) and adding the use of CONCAT, GROUP_CONCAT and CAST (to mix int and varchar).

I upvoted both answers, which solved the problem partially, but I thought I should post my own answer since (I think) is a cleaner solution, at least for what I wanted, and it may help someone else.

Here's the SQL

SELECT 
  a.id, 
  a.title, 
  GROUP_CONCAT( DISTINCT CAST( t.id AS CHAR( 50 ) ) ,  ':', t.name ORDER BY t.id SEPARATOR  ',' ) AS  'tags', 
  GROUP_CONCAT( DISTINCT CAST( i.id AS CHAR( 50 ) ) ,  ':', i.fileName ORDER BY i.id SEPARATOR  ',' ) AS  'images'
FROM 
  articles a
JOIN 
  article_tags ta ON a.id = ta.articleId
JOIN 
  images i ON a.id = i.articleId
JOIN 
  tags t ON ta.tagId = t.id
WHERE 
  a.id = :id

This query returns the following, with id = 1:

enter image description here

A single row containing the article's data, its tags and images in a id:value format, which I think it's very easy to parse in PHP and can even be used to return a json string by just adding another concat.

Upvotes: 1

Adrian Cid Almaguer
Adrian Cid Almaguer

Reputation: 7791

Try this code:

EDIT:

$sql = "SELECT
articles.id, 
articles.title, 
articles.dateCreated as articleDate,
article-images.id as imageId,
article-images.fileName as imageFile,
tags.id as tagId,
tags.name as tagName
FROM articles
JOIN tag-article ON articles.id = tag-article.articleId
JOIN tags ON tag-article.tagId = tags.id
JOIN article-images ON article-images.articleId = articles.id
WHERE articles.id=:id"

$query = $this->db->prepare($sql);
$parameters = array(':id' => $id);
$result = $query->execute($parameters);
return ($result) ? $query->fetch() : 0;

?>

Upvotes: 1

Daniel Stancu
Daniel Stancu

Reputation: 62

SELECT 
    a.id, 
    a.title, 
    a.dateCreated as articleDate,
    i.fileName as imageFile,
    t.name as tagName
FROM 
    articles a
JOIN 
    images i ON a.id = i.articleId
JOIN 
    tag_article ta ON a.id = ta.articleId
JOIN
    tags t ON ta.articleId = t.id
WHERE 
    a.id = ?

Upvotes: 1

Related Questions