Beginner Rails Question.
I have a table of Users and a table of Teams.
A User has many teams and Teams belong to User.
I want to query if a user does not have a team.
I'm using this query:
User.joins(:teams).where.not(teams: {team_name: 'coconuts'})
This works except if the user has more than one team.
For example User Bill is on the coconuts team and the breadfruit team.
The above query returns Bill when he should be excluded because he is on the coconuts team.
I see why this is happening but I'm having trouble thinking of another query that will work for this scenario.
What is the correct way to grab this data?
I'm using Rails 4.
Try to the following, please consider simple and clean code vs performance:
team = Team.find_by(name: 'coconuts')
excluded_user_ids = team.user_ids
User.where.not(id: excluded_user_ids)
# If you want more a little bit efficiently and suppose you have the join model `Membership`
excluded_user_ids = team.memberships.pluck(:user_id)
# Or if you want more efficiently (just 1 query) and suppose you're using Postgresql
User
.left_outer_joins(:teams)
.group('users.id')
.select("users.*, count(teams.id) AS count_foo, count(teams.id) filter (where teams.name = 'coconuts') AS count_bar")
.having('count_foo != count_bar')
Using just Ruby, and not active record, you can do
User.select {|user| user.teams.pluck(:team_name).exclude?('coconuts')
}
I've been trying to implement an authorization layer on top of ActiveRecord. Let me explain how that is supposed to work.
Databases
Consider a database table Invoices with the following fields
InvoiceId
CustomerId
... other fields
There will be an auxiliary table InvoicePrivileges with the fields
ObjectId (referring to an invoice id)
SubjectId (referring to a customer id in this case)
Subject type (to handle multiple kinds of users - customer, admin, operator, etc)
Read (boolean)
Write (boolean)
Authorization checks
To be able to read an invoice, the entity attempting to read the row or set of rows must have a set of entries in the InvoicePrivileges table (where InvoicePrivileges.object_id refers to an InvoiceId) with InvoicePrivileges.read = true.
Example, a query to fetch a bunch of invoices from the DB
SELECT invoice.*
FROM Invoices invoice
LEFT JOIN InvoicePrivileges privilege
ON invoice.invoice_id = privilege.object_id
AND privilege.subject_id = <user_id>
AND privilege.subject_type = <user_type>
WHERE privilege.read = TRUE;
The same condition applies when trying to update an invoice, except the last WHERE condition becomes WHERE privilege.write = true.
Implementation
I can use the Arel library to create these constraints with ease. However, where do I implement these methods in such a way that all ActiveRecord save and update actions include these constraints?
I don't mind writing a bit of code to enable this. I'm looking for pointers as to how best to go about it.
Create a gem and require it after ActiveRecord gem. In your gem open up ActiveRecord classes and override the interested methods.
module ActiveRecord
class Base
def save(*)
if(authorised?)
super
else
raise NotAuthorisedError.new("Your meaningful message!")
end
end
end
end
The original code from Rails source is in Persistence class. All persistence methods usually use create_or_update method internally. So, you might consider overriding that method instead. But, you need to look through the code and see if that's a good idea.
TL;DR? See Edit 2
I've got a little Rails application that has a few different sort of games people can play: it's based around sports, so they can pick the winners of each game every week (model PickEm, attribute correct boolean with nil for unfinished games), and predict the outcome of a specific team's game (model Guess, attribute score with integer, nil for unfinished games). Every User has_many PickEms and Guesses. And I'm trying to display standings (correct/total - total being all non-nil, score/total possible).
What I'm finding is that I can gather the users and their associated records, but in trying to display standings I'm discovering that every single User is triggering another query - slow and not sustainable as the user base increases. That's because #user.pick_em_score is pick_ems.where(correct: true).size and #user.guess_Score is guesses.where.not(score: nil).sum(:score). So I call user.pick_em_score and it runs that query. I feel like there should be a way to get every User, as well as these specific counts, at once, rather than buffering a whole bunch of needless extra stuff.
What I need:
User record
User.pick_em_score (calculated by counting correct records)
User.pick_ems count where NOT NULL
User.guesses_score (calculated by guesses.sum(:score))
User.guesses count where NOT NULL
Most of the stuff I find on Rails's ActiveRecord helpers, especially related to calculations, is for retrieving only the calculation. It looks like I'll probably need to delve directly into select() etc. But I can't get it working. Can someone point me in the right direction?
Edit
For clarification: I'm aware that I can write this information to the User model, but this is overly restrictive: next season, I'll need to add a new column to the User for that year's results, etc. In addition, this is a third degree of callback updating related models – the Match model already updates related PickEms and Guesses on save. I'm looking for the simplest ActiveRecord query or queries to be able to work with this information, as indicated by the title. Ideally one query that returns the above information, but if it needs to a few, that's OK.
I used to work directly in MySQL with PHP, but those skills have rusted (in raw MySQL, I imagine, I'd have several sub-select statements to help pull these counts) and I'd also like to be able to use Rails's ActiveRecord helpers and such, and avoid constructing raw SQL as much as possible.
Second Edit:
I seem to have it down to one call that starts to work, but I'm writing a lot of SQL. It's also brittle, IMO, and trying to run with it has failed. It also looks like I'm just pushing the million singular SELECT queries from Rails right into SQL, but that may still be a step up.
User.unscoped.select('users.*',
'(SELECT COUNT(*) FROM pick_ems WHERE pick_ems.user_id = users.id AND pick_ems.correct) AS correct_pick_ems',
'(SELECT COUNT(*) FROM pick_ems WHERE pick_ems.user_id = users.id AND pick_ems.correct IS NOT NULL) AS total_pick_ems',
'(SELECT SUM(guesses.score) FROM guesses WHERE guesses.user_id = users.id AND guesses.score IS NOT NULL) AS guesses_score',
'(SELECT COUNT(*) FROM guesses WHERE guesses.user_id = users.id AND guesses.score IS NOT NULL) AS guesses_count' )
The issue seems to be: is there a way to use Rails, and not raw SQL, to link up users.id that we see there with these subqueries? Or just … a better way to construct this, in general?
In addition, I'm running another set of SELECTs for the WHERE, which would hinge on total_pick_ems and guesses_count being > 0 but since I can't use those aliased columns, I have to call the SELECT one more time.
Welcome to AR. Its really only good for simple CRUD like queries. Once you actually want to query your data in anger it just doesn't have the capababilities to do the queries you want without resorting to wholesale SQL strings and often abandoning the ability to chain as a result.
Its precisely why I moved to Sequel as it does have the features to compose queries using a much fuller SQL feature set, including join conditions, window functions, recursive common table expressions, and advanced eager loading. The author is incredibly responsive and documentation is excellent compared to AR and Arel.
I don't expect you will like this answer but a time will come when you will start to look outside the opinionated components that come with rails which I have to say are hardly best of breed. Sequel also sped my application up many times over what I was able to get with AR as well, it not just developer happiness, it means less servers to run. Yes it will be a learning curve but IMO its better to learn tools that have your back covered.
Joins might work. Smthing like below
User.unscoped.joins(:guesses).joins(:pick_ems).
where("guesses.score IS NOT NULL").
select("users.*,
sum(guesses.score) as guesses_score,
count(guesses.id) as guesses_count,
count(case when pick_ems.correct = True then 1 else null end)
as correct_pick_ems,
count(case when pick_ems.correct != null then 1 else null end)
as total_pick_ems,
").
group("users.id")
If you need this information for a limited number of users at a time then above query or eager loading (User.includes(:guesses, :pick_ems)) with class methods like
def correct_pick_ems
pick_ems.count(&:correct)
end
would work.
However If you need this information for all the users most of the time, cached counters within the users table would be more optimal.
What you need is some sort of custom (smart) counter_cache to count only at certain conditions (e.g correct is true)
You can achive this using conditional after_save & after_destroy triggers to build your own custom counter_cache that looks like this:
class PickEm
belongs_to :user
after_save :increment_finished_counter_cache, if: Proc.new { |pick_em| pick_em.correct }
after_destroy :decrement_finished_counter_cache, if: Proc.new { |pick_em| pick_em.correct }
private
def increment_finished_counter_cache
self.user.update_column(:finished_games_counter, self.user.finished_games_counter + 1) #update_column should not trigger any validations or callbacks
end
def decrement_finished_counter_cache
self.user.update_column(:finished_games_counter, self.user.finished_games_counter - 1) #update_column should not trigger any validations or callbacks
end
end
Notes:
Code not tested (only to show the idea)
Some guys said it's better to avoid naming custom counters as rails name them (foo_counter_cache)
You should benchmark it, but my hunch is that adding all of that data into a single SELECT isn't going to be much faster than breaking it up into separate SELECTs (I've actually had cases where the latter was faster). By breaking it up, you can also stick to more ActiveRecord and less raw SQL, e.g.:
user_ids_to_pick_em_score = User.joins(:pick_ems).where(pick_ems: {correct: true}).group(:user_id).count
user_ids_to_pick_ems_count = User.joins(:pick_ems).where.not(pick_ems: {correct: nil}).group(:user_id).count
user_ids_to_guesses_score = Hash[User.select("users.id, SUM(guesses.score) AS total_score").joins(:guesses).group(:user_id).map{|u| [u.id, u.total_score]}]
user_ids_to_guesses_count = User.joins(:guesses).where.not(guesses: {score: nil}).group(:user_id).count
Edit: To display them, you could do like so:
<%- User.select(:id, :name).find_each do |u| -%>
Name: <%= u.name %>
Picks Correct: <%= user_ids_to_pick_em_score[u.id] %>/<%= user_ids_to_pick_ems_count[u.id] %>
Total Score: <%= user_ids_to_guesses_score[u.id] %>/<%= user_ids_to_guesses_count[u.id] %>
<%- end -%>
I would like to be able to sign a user in with omniauth and devise from multiple accounts. In my user model I have set the twitter user's uid to equal uid and the provider = twitter. For the user's Facebook uid would equal uid2 and provider2 = Facebook.
I currently use this query to either sign a user in if auth.slice = provider, uid or register them if not.
where(auth.slice(:provider, :uid)).first_or_create do |user|
However now I want to run something similar to:
where(auth.slice(:provider, :uid || :provider2, :uid2 )).first_or_create do |user|
I.e where auth.slice = :provider, :uid OR :provider2, :uid2
essentially I need to be able to run an OR query.
Any help will be much appreciated.
Firstly, the auth.slice(:provider, :uid) is referring to keys in the auth hash from omniauth but provider2 and uid2 are only columns in your db, so they can't be used in the slice in any way as they aren't keys in the auth hash (though I realise that was only an illustration/example of yours). You'd need to extract the hash entries and compare with provider and uid columns and provider2 and uid2 columns separately. Also, to do an OR, you could either use the old-school approach of SQL snippets:
Users.where('(provider = :provider AND uid = :uid) OR (provider2 = :provider AND uid2 = :uid', {provider: auth[:provider], uid: auth[:uid]})
or use AREL, as the bog-standard where can't handle OR. The AREL readme is quite readable https://github.com/rails/arel -- you could do something like:
prov = auth[:provider]
uid = auth[:uid]
users = Arel::Table.new(:users)
users.where(users[:provider].eq(prov).and(users[:uid].eq(uid))).or(users[:provider2].eq(prov).and(users[:uid2].eq(uid)))).project(Arel.sql('*'))
However, I think the long-windedness of both of these highlights that the approach of adding sets of columns per-provider makes things a bit of a pain and it would get worse if, for example, you add login via google, yahoo or whatever, resulting in provider3, uid3 and provider4, uid4... An alternative is to have another model/table which stores these different identities, with a has_many relationship with users. For example:
class User < ActiveRecord::Base
has_many :identities
...
end
class Identity < ActiveRecord::Base
belongs_to :user
...
end
Then you could look up by identities using a simple
Identity.where(auth.slice(:provider, :uid))
like you do now, and not have to worry about adding new providers. They would just result in another row in the identies table related to the relevant user. You could join with users or however you want to handle getting the associated user.
I have this query put together which performs fine.
search = Artist.where(:status => "active").where("upcoming_events_count > 0").order("echonest_external_popularity DESC").limit(limit))
The issue is that I'd like to add one more check, which is on another table ArtistPhoto. The match needs to make sure that the Artist (from the artist_id) also contains a valid photo.
I have the query working find independently:
ArtistPhoto.where("artist_id = ? and artist_photos.primary = ?", self.id, true).first
If anyone can assist with getting the join into 1 query that would be great.
Thanks
Use joins to add the other association:
search = Artist.joins(:artist_photos).
where("artist_photos.primary = ?", true).
where(...) # other filters here
This answer assumes you have a has_many :artist_photos on your Artist model.