0

I'm trying to do something very simple in CF/SQL but just can't seem to figure out what I am doing wrong.

I have these tables:

movies       genres     actors      moviesGenres     moviesActors
------       ------     ------      ------------     ------------
movieId      genreId    actorId     id               id
title        name       fname       movie            movie
                        lname       genre            actor

I'm trying to write a query that simply lists all movies, with each movie's genre (multiple selections possible) and each movie's actors (again, multiple selections possible).

When I write the query to include just the genres, all works fine. I use for the query itself, grouping by movieId, and then a separate around the genres.

But when I try to then include the actors, everything blows up and it seems as if the grouping breaks down.

Here's the SQL query for both joins:

SELECT m.movieId, m.title, m.releaseDate, m.description, g.name, a.fname, a.lname
FROM movies m
   INNER JOIN genres g ON g.genreId IN (SELECT genre FROM moviesGenres WHERE movie = m.movieId)
   INNER JOIN actors a ON a.actorID IN (SELECT actor FROM moviesActors WHERE movie = m.movieId)
ORDER BY m.title

Thanks in advance for any help!

UPDATE:

The query supplied by Leigh and Mark seems to work overall, but I am still seeing the actors displayed multiple times in the . Here is my code:

<tbody>
   <cfoutput query="variables.movieList" group="movieId">
      <tr>
         <td><a href="##">#title#</a></td>
         <td><cfoutput group="name">#name# | </cfoutput></td>
         <td><cfoutput group="actorId">#actorId# | </cfoutput></td>
      </tr>
   </cfoutput>
</tbody>

I've also tried it without grouping the final tag but that didn't work. Note that I changed the a.lName and a.fName to a.actorId for the sake of simplicity in testing.

A sample row looks like this:

The Godfather    Action | Drama |    1 | 2 | 1 | 2 |
Gary
  • 453
  • 4
  • 15

4 Answers4

3

You have to relate all of the tables within the join. Otherwise you end up with a cartesian product. So JOIN to the junction tables as well, instead of using them in a subquery. With the correct ORDER BY you should be able to use <cfoutput group=".."> to format the output as desired.

Not tested, but something along these lines.

SELECT m.movieId, m.title, m.releaseDate, m.description, g.name, a.actorID, a.fname, a.lname
FROM movies m
        LEFT JOIN moviesGenres mg ON mg.movie = m.movieID
        LEFT JOIN genres g ON g.genreID = mg.genre
        LEFT JOIN moviesActors ma ON ma.movie = m.movieID
        LEFT JOIN actors a ON a.actorId = ma.actor
// since movie names may not be unique, do a secondary sort on movieID
ORDER BY m.title, m.movieID, g.Name, a.fName, a.lName

Edit based on comments:

As Steve mentioned, when using cfoutput group the results must be sorted and grouped the same way for the feature to work properly. See his answer for a more detailed explanation.

An alternative to approach is to use structures to generate the unique genres and actors. Note, the sql query above was modified slightly to match your updated example.

<table border="1">
<!--- group by ID in case multiple movies have the same name --->
<cfoutput query="yourQuery" group="movieID">

    <!--- use structures to save unique genres and actors --->
    <cfset genres = {}>
    <cfset actors = {}>
    <cfoutput>
        <cfset genres[name] = true>
        <cfset actors[actorID] = true>
    </cfoutput>

    <!--- display results --->
    <tr><td>#Title#</td>
        <td>#structKeyList(genres, "|")#</td>
        <td>#structKeyList(actors, "|") #</td>
    </tr>
</cfoutput>
</table>
Community
  • 1
  • 1
Leigh
  • 28,765
  • 10
  • 55
  • 103
  • Lol .. yours is a thing of beauty as well ;) – Leigh Apr 12 '12 at 15:13
  • One thing to think about when comparing options is that while mine and Leigh's are similar in length, Leigh's will be comparibly more readable and concise if you continue to need to add more detail fields like "genre" and "actor". (so I suggest you choose Leigh's) – Steve Bryant Apr 12 '12 at 20:18
2

Not sure I like your syntax. I'm guessing your subselects and aliasing are screwing with your results. What you are after is to join these bridge tables if I catch what you are doing. I would do something like this:

SELECT m.movieId, m.title, m.releaseDate, m.description, g.name, a.fname, a.lname
FROM movies m
  INNER JOIN movieGenres mg ON m.movieID = mg.movie
  INNER JOIN genres g ON g.genreID = mg.genre
  INNER JOIN movieactors ma on ma.movie  = m.movieID
  INNER JOIN actors a on ma.actor = a.actorID
ORDER BY m.title

This would work if actors.movie and genres.movie both match movies.movieID - but normally I like to see foreign keys named better than that. For example, if Actor.movie really contains the "movieID" then name it "movieID" - otherwise I think I'm looking for

Leigh
  • 28,765
  • 10
  • 55
  • 103
Mark A Kruger
  • 7,183
  • 20
  • 21
  • FYI - Leighs answer might give you a more flexible or complete result set. an inner join will return movies who have at least one record in all 4 tables while a left join will allow for a movie that doesn't have a genre or an actor mapped to it. – Mark A Kruger Apr 12 '12 at 15:13
  • 1
    +1 for the comment about naming conventions. Maintaining a consistent naming convention keeps things more intuitive, which is especially important with large schemas. – Leigh Apr 12 '12 at 15:16
  • This query seemed to help a lot, but I am still seeing multiple outputs on the actor. I've updated the original post above. Thanks to both you and Mark! – Gary Apr 12 '12 at 16:16
  • Yes, when using `group` how you sort the query results is critical. You must ORDER the same way you are "grouping" the cfoutput. Otherwise, the `group` feature does not work properly. See Steve's response for a more detailed explanation. – Leigh Apr 12 '12 at 17:59
2

Your question really has two separate issues.

1) How to write a SQL query to get the results needed for your page.

Mark and Leigh both have good solutions for this problem. Personally I prefer Leigh's.

I will just add to dump your results as you go (all here probably know that, but a good thing to have documented for posterity) as it is easy to assume that you are getting different results than you think.

Also, it is particularly relevant for the next step.

2) How to display the results correctly.

For this, you have to understand how the "group" attribute of cfoutput works. It simply check if the value for the column you selected changes from the previous row. This means that you must order by the group column or else the grouping will look wrong.

For example:

category,product

  • Food, Apple
  • Cleaners, Comet
  • Food, Banana

If you did on the results above it would look wrong because the grouped output would show up every time the category switched. The following resultset, on the other hand, would work correctly:

  • Cleaners, Comet
  • Food, Apple
  • Food, Banana

The upshot of this is that cfoutput group depends on the ordering and so you can only use it on one column for any one level (though, of course, you can go as many levels deep as you would like).

The solution, then, is to handle the second column grouping more manually. This can be by building up a list somewhere and then looping over it, for example.

So, for the query in your example, this would work:

<tbody>
   <cfoutput query="variables.movieList" group="movieId">
       <cfset actors = "">
        <cfoutput>
            <cfif NOT ListFindNoCase(actors,actorId)>
                <cfset actors = ListAppend(actors,actorId)>
            </cfif>
        </cfoutput>
      <tr>
         <td><a href="##">#title#</a></td>
         <td><cfoutput group="name">#name# | </cfoutput></td>
         <td><cfloop list="actors" index="actor">#actor# | </cfloop></td>
      </tr>
   </cfoutput>
</tbody>
Steve Bryant
  • 1,046
  • 5
  • 7
  • +1. It is important to be aware that limitation of `cfoutput group`. "Grouping" in parallel requires a bit more creativity. – Leigh Apr 12 '12 at 20:31
  • @Steve - thanks for the info and code. I understand much better now how grouping works. I appreciate everyone's help. – Gary Apr 13 '12 at 14:36
0

You can't do it with both actors and genres in the same query. Your best bet is to output just the movies with <cfoutput query="someQry" group="movieId"> and then do a query-of-queries to get the actors and genres for each movie seperately.

Jan Brünemann
  • 229
  • 1
  • 11
  • No, it is possible with the correct join and multiple `group` clauses in the `cfoutput`. – Leigh Apr 12 '12 at 15:32
  • It may be possible, but your code will be more error prone and more difficult to maintain. Query-of-query is very fast and makes your code much more readable and flexible. – Jan Brünemann Apr 13 '12 at 06:58
  • No, `group` is like any other feature. It is not error prone, unless maybe you misunderstand how the feature operates. The `group` feature often provides a simpler alternative to QoQ's with nested loops. My experience has been that QoQ's are not always faster. I imagine it is the overhead of building a full query object on every iteration. So I tend to avoid it when possible. – Leigh Apr 13 '12 at 14:23
  • Well I respectfully disagree with your disagreement ;) Since it is a matter of presentation, I think the grouping logic is where it should be: in the output code, not in SQL. I posted one approach to this problem above (see my updated response.) – Leigh Apr 13 '12 at 14:42