SELECT
posts.*,
(SELECT COUNT(1) FROM comments WHERE post_id = posts.id) AS comments_count
FROM posts;
In ActiveRecord, without 1+N queries, or caching comments_count in a column somewhere?Admittedly, that was not the best example. The last time I need something more intertwined than a simple COUNT in subquery, the answer was "give up and just use Arel." But at this point it is no longer quite ActiveRecord, but rather a SQL without strings.
This is my biggest gripe against the Active Record pattern in general, as it ties its model too tightly to the underlying database. It is convenient for a simple CRUD tasks, which may fit about 90% of use case, but that's not the only thing the database is capable of.
Post.objects.all().annotate(Count('comments'))
Which produces (simplified, Django would actually explicitly select each column): SELECT posts.*, COUNT(comments.id) AS comments_count
FROM posts
LEFT OUTER JOIN comments ON (posts.id = comments.post_id) GROUP BY posts.id
Nice, easy and without 1+N queries.I'm surprised there isn't more explicit support for this in more ORMs (things like not having 'save' methods on the ORMed classes).
For example, let's say you have a query that gets search results, and depending on whether the visitor is logged in or not you also may want to know whether a given search result happens to be a favourited item. The way I understand it, I would have to do for example:
$query_text = 'blah';
if (user is logged in)
{
$query_text .= 'SQL pertaining to favourites';
}
$stmt = $dbh->prepare($query_text);
if (user is logged in)
{
$stmt->bindParam(':user_id', $user_id);
}
Or am I missing something?What really bugged me though was the idea of having to repeat the decision logic that determines what the query ends up being twice... though I just realized that putting name/value pairs into an array while building the query, and then using that when the query is built to bind them to it, is probably fine.
function getResultsForUser($DB, $user_id)
{
$query="search for user :user_id, SQL pertaining to favourites";
$stmt=$DB->prepare($query);
$stmt->execute(["user_id"=>$user_id]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function getResultsForVisitor($DB)
{
$query="blah";
$stmt=$DB->prepare($query);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
Yes, it does mean more code, but that code will be much cleaner, more readable, more easily transportable (by not depending on a third party library) and less bug-prone. - is the user logged in, and maybe even owner of the profile we're at?
- are we looking at all nodes, or the subnodes/subtree (don't ask) of a node?
- do we want to display tags/authors/sources?
- are we filtering by tag/author?
- what's the display mode: list or full?
There might be more too, that's from the top of my head... Yes, it's messy, but also kina DRY. To "unroll" that would be even more monstrous, that can't be the way D:But as I said in my other comment, I realized I can just remember the parameters/values as I build the query string, and then bind them all after creating the query. That way I can have my messy cake (that should feel so wrong but tastes so right) and eat it with a plate and a fork like a proper person.
Post.select("posts.*, count(comments.*) as comments_count").joins(:comments)Unfortunately, what I needed to do back then were more complex than that (say, I need to perform query on top of subquery, e.g. "SELECT ... FROM (SELECT ...) AS a HAVING ... GROUP BY ..." sort of thing) which seems to be something ActiveRecord wasn't designed for.
In the end, I solve the problem by using Arel with an ActiveModel and unwrap all the data manually.
Post.select("posts.*, COUNT(comments.*) AS comments_count").joins("LEFT JOIN comments ON comments.post_id = posts.id").group(:comments)