Recently, in a moment of well-timed stubbornness, I came across arel’s ability to merge scopes. Merging greatly expands the ways in which you can apply scopes, and so too the kinds of logic you can put in them. As far as I can see, though, they’re not very widely used. In fact, the only place I’ve seen them referred to is in this excellent railscast by Ryan Bates. Well, here’s my sell.
An arel query’s starting point determines the result: you base each query on the model whose records you want in the results. Whether you’re joining or including, or supplying nested conditions, you get a list of user records by starting your query on
User (or one of its scopes). This is a good pattern, but I’ve long wondered how to also involve scopes from other models.
If I wanted to employ a scope defined on a model I was joining to, then up until last week I thought I was up the creek, but this is what
merge makes possible. To illustrate the solution, here’s a stylised portion of the data model that backs our publishing platform at The Conversation.
To find all the admin users of a group, we want to start on the result model (
User), even though the role information is stored on
Collaboration. In the past, I would have written this query:
This produces nice SQL, but we had to duplicate that
#where logic from
Collaboration.editorial. Surely it’s better to keep things dry?
Hey, at least we got to use our
editorial scope, right?
Turns out, you can compose the proper query by re-using that scope, even though we’re quering it’s not defined on
Combining queries like this (I believe the technical term is smooshing—the queries have been smooshed together) means you can re-use the scopes you have all over the place. Even better,
merge lets you push much more query logic into scopes than you otherwise could. You win on two fronts: keeping your querying logic dry in this case means using your DB like a real DB, too.
There’s one thing to be aware of here: when you start re-using scopes in this way, particularly across models, you run the risk of coupling your models. My feeling about this is that it’s not a problem as long as non-trivial scopes are specced, and that model-specific logic is wrapped up in a scope on that model. As long as a scope’s spec breaks when one of the scopes it depends on changes, I feel comfortable with this kind of declarative coupling.
Merging scopes works with associations, too: when you merge an association, you merge the condition that defines it, as well as the attached scopes.
Suppose we want to retrieve all the figures associated with the drafts that a given user is editing. Our constraints: we have to start on
Figure because figures are what we want, and we want to pull in logic from
Note well: this isn’t a class-level scope, it’s a list of figures corresponding to a specific user. Even so, we started the query
Figure-wide, and scoped it to the user by merging the
collaborations association. That’s what narrows this query to the user in question.
We’ve just recently done some refactoring across our codebase to employ
merge, and I’ve been really pleased with how a lot of complex logic has fallen away. When you can re-use scopes anywhere, you have a lot more freedom to define them cleanly and simply.