简体   繁体   中英

Is there an SQL query for retrieving data that has been joined from multiple tables?

I am learning how to work with SQL to put together a database of information on books I own. Sometimes books can be written by multiple authors, be a part of multiple series, and be categorized by multiple genres. It is my understanding that the best way to handle array-like data in relational databases is by using separate tables.

My database looks like this:

Books

Key Field Type
PK id INT
title VARCHAR(255)
summary VARCHAR(1000)
pages INT
published_date DATE
ddc_number VARCHAR(20)
age_rating INT
reviews_rating INT

[ Genres | Series | Authors ] Three separate tables.

Key Field Type
PK id INT
name VARCHAR(255)

[ Genres List | Series List | Authors List ] Three separate tables. Each genre/series/author for a book is stored in an individual row.

Key Field Type
FK book_id INT
FK genre_id/series_id/author_id INT

I am building an API for retrieving book data from the database. The code used for retrieving books is as follows:

const express = require('express');

const Router = express.Router();

const mysqlConnection = require('../connection');

Router.get('/all', (req, res) => {
  let results = {};

  mysqlConnection.query('SELECT * FROM book', (bookErr, bookRows, bookFields) => {
    if (bookErr) { console.log(bookErr); return; }

    results = bookRows;

    mysqlConnection.query('SELECT genre.name FROM book_genre JOIN genre ON genre.id = book_genre.genre_id WHERE book_genre.book_id = 1', (genreErr, genreRows, genreFields) => {
      if (genreErr) { console.log(genreErr); return; }

      results[0].genres = genreRows;

      mysqlConnection.query('SELECT author.name FROM book_author JOIN author ON author.id = book_author.author_id WHERE book_author.book_id = 1', (authorErr, authorRows, authorFields) => {
        if (authorErr) { console.log(authorErr); return; }
  
        results[0].authors = authorRows;

        mysqlConnection.query('SELECT series.title FROM book_series JOIN series ON series.id = book_series.series_id WHERE book_series.book_id = 1', (seriesErr, seriesRows, seriesFields) => {
          if (seriesErr) { console.log(seriesErr); return; }
    
          results[0].series = seriesRows;
    
          res.send(results);
        });
      });
    });
  });
});

module.exports = Router;

This code gives the output that I want:

[
    {"id":1,
    "title":"Harry Potter and the Sorcerer's Stone",
    "summary":"When mysterious letters start arriving on his doorstep, Harry Potter has never heard of Hogwarts School of Witchcraft and Wizardry.\n\nThey are swiftly confiscated by his aunt and uncle.\n\nThen, on Harrys eleventh birthday, a strange man bursts in with some important news: Harry Potter is a wizard and has been awarded a place to study at Hogwarts.\n\nAnd so the first of the Harry Potter adventures is set to begin.",
    "pages":223,
    "published_date":"1997-06-26T04:00:00.000Z",
    "ddc":"823.914",
    "age_rating":12,
    "reviews_rating":89,
    "genres":[
        {"name":"Fantasy"},
        {"name":"Fiction"}
    ],
    "authors":[
        {"name":"J.K. Rowling"}
    ],
    "series":[
        {"title":"Harry Potter"}
    ]
}
]

Is there a simplified way of being able to retrieve a book along with its genres, series, and authors?

I used GROUP_CONCAT along with the suggested query to combine multiple rows of results in to one:

SELECT book.*,
GROUP_CONCAT(author.name) as 'authors',
GROUP_CONCAT(series.title) as 'series',
GROUP_CONCAT(genre.name) as 'genres'
FROM book
LEFT OUTER JOIN book_author ON book.id=book_author.book_id
JOIN author ON book_author.author_id = author.id
LEFT OUTER JOIN book_series ON book.id=book_series.book_id
JOIN series ON book_series.series_id = series.id
LEFT OUTER JOIN book_genre ON book.id=book_genre.book_id
JOIN genre ON book_genre.genre_id = genre.id;

Given 3 genres, 1 author, and 1 series... the resulting output is:

[
    {"id":1,
    "title":"Harry Potter and the Sorcerer's Stone",
    "summary":"When mysterious letters start arriving on his doorstep, Harry Potter has never heard of Hogwarts School of Witchcraft and Wizardry.\n\nThey are swiftly confiscated by his aunt and uncle.\n\nThen, on Harrys eleventh birthday, a strange man bursts in with some important news: Harry Potter is a wizard and has been awarded a place to study at Hogwarts.\n\nAnd so the first of the Harry Potter adventures is set to begin.",
    "pages":223,
    "published_date":"1997-06-26T04:00:00.000Z",
    "ddc":"823.914",
    "age_rating":12,
    "reviews_rating":89,
    "genres": "Young Adult,Fiction,Fantasy",
    "authors": "J.K. Rowling,J.K. Rowling,J.K. Rowling",
    "series": "Harry Potter,Harry Potter,Harry Potter"
}
]

I think I understand why it is repeating results. 4 genres would cause 1 author and 1 series to repeat 4 times. Is there a solution to stop that from happening?

You can combine the four database queries into one:

SELECT book.*, genre.name, author.name, series.title
FROM book
JOIN book_genre ON book.id = book_genre.book_id
JOIN genre ON genre.id = book_genre.genre_id
JOIN book_author ON book.id = book_author.book_id
JOIN author ON author.id = book_author.author_id
JOIN book_series ON book.id = book_series.book_id
JOIN series ON series.id = book_series.series_id

This would give you a results table with two rows (because of the two genres) and redundant entries in the other columns. If the book had two authors and three series, you would get 2*2*3=12 rows. Transforming that into the JSON format you expect is a bit of work, but thanks to the fewer database calls it should be faster overall.

Using the MySQL GROUP_CONCAT() function along with its DISTINCT attribute will return a single row from a query with the requested column values concatenated together.

This answer returns a single row with all the genres, authors, and series instead of multiple rows of redundancy.

Code solution:

const express = require('express');

const Router = express.Router();

const mysqlConnection = require('../connection');

Router.get('/all', (req, res) => {
  mysqlConnection.query(`
    SELECT book.*,
    GROUP_CONCAT(DISTINCT author.name) as 'authors',
    GROUP_CONCAT(DISTINCT series.title) as 'series',
    GROUP_CONCAT(DISTINCT genre.name) as 'genres'
    FROM book
    LEFT OUTER JOIN book_author ON book.id = book_author.book_id
      JOIN author ON book_author.author_id = author.id
    LEFT OUTER JOIN book_series ON book.id = book_series.book_id
      JOIN series ON book_series.series_id = series.id
    LEFT OUTER JOIN book_genre ON book.id = book_genre.book_id
      JOIN genre ON book_genre.genre_id = genre.id
    GROUP BY book.title
  `, (err, rows, fields) => {
    if (err) { console.log(err); return; }
    res.send(rows);
  });
});

module.exports = Router;

GROUP BY book.title will return all books instead of just the last one.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM