why does Rails think Model attribute is a method? - ruby-on-rails

This is a custom rake task file, if that makes a difference.
I want to pull all user_id's from Pupil, and apply them to get the User.id for all pupils.
It happily prints out line 2 with correct user_id, but then considers user_id a 'method' and breaks on line 3. Why? Where is my mistake?
course_pupils = Pupil.where(course_id: study.course_id)
course_pupils.map { |a| puts a.user_id }
pupils = User.where(id: course_pupils.user_id )

course_pupils is still a relation when you are calling it in line 3. Line 2 is non destructive (and if it was, it would turn it into an array of nils because puts returns nil).
You need to do:
pupils = User.where(id: course_pupils.pluck(:user_id) )
Or something like that

You are doing it wrong, you cannot call an instance method user_id on a collection, try this instead
user_ids = Pupil.where(course_id: study.course_id).pluck(:user_id)
pupils = User.where(id: user_ids )
Hope that helps!

Related

first_or_create only insert

I'm using the following code in a Rails 5.2 project:
c = Country.where(name: "test").first_or_create
c.status = "Old"
c.save
This works, however, I only want to change the status when there is not a country already (so I only want to insert countries, not update). However, I also need to use c later in my code.
I realize I could just do an if statement, but I have to do a similar thing multiple times in my code, and it seems like a shortcut must exist.
Use create_with by following:
c = Country.create_with(status: 'old').find_or_create_by(name: 'test')
This will create Country with status 'old' and name 'test' only and only if it doesn't find the country with name 'test'. If it does find the country with name 'test', it will not update the status.
Ultimately, it will also return the country in c, whether after finding or creating.
create_with only sets attributes when creating new records from a relation object.
The first_or_create method finds the result in table and If it exists in the table, the first instance will be returned. If not, then create is called.
If a block is provided, that block will be executed only if a new instance is being created. The block is NOT executed on an existing record.
So If you only want to change status when country is created then use block with first_or_create method.
so your code looks like
Country.where(name: "test").first_or_create do |country|
country.status = "Old"
country.save
end
By default where returns array type and you will need to iterate to assign a new value.
And find_by returns actual object or nil.
Country.where(name: "nothing") => [] or [obj, ...]
Country.find_by(name: "nothing") => nil or object
You can use find_or_create option:
Country.find_or_create_by(name: 'test') do |c|
c.status = "Old"
end
c = Country.find_or_create_by(name: 'test')
c.status = "Old"
c.save

Fetch ActiveRecord query result as an array of hashes with chosen attributes

The model User has first, last and login as attributes. It also has a method called name that joins first and last.
What I want is to iterate through the Users records and create an array of hashes with the attributes I want. Like so:
results = []
User.all.map do |user|
record = {}
record["login"] = user.login
record["name"] = user.name
results << record
end
Is there a cleaner way in Ruby to do this?
Trying to map over User.all is going to cause performance issues (later, if not now). To avoid instantiating all User objects, you can use pluck to get the data directly out of the DB and then map it.
results = User.all.pluck(:login, :first, :last).map do |login, first, last|
{ 'login' => login, 'name' => first << last }
end
Instantiating all the users is going to be problematic. Even the as_json relation method is going to do that. It may even be a problem using this method, depending on how many users there are.
Also, this assumes that User#name really just does first + last. If it's different, you can change the logic in the block.
You can use ActiveRecord::QueryMethods#select and ActiveRecord::Relation#as_json:
User.select(:login, '(first || last) as name').as_json(except: :id)
I would write:
results = User.all.map { |u| { login: u.login, name: u.name } }
The poorly named and poorly documented method ActiveRecord::Result#to_hash does what you want, I think.
User.select(:login, :name).to_hash
Poorly named because it does in fact return an array of Hash, which seems pretty poor form for a method named to_hash.

Equivalent of find_each for foo_ids?

Given this model:
class User < ActiveRecord::Base
has_many :things
end
Then we can do this::
#user = User.find(123)
#user.things.find_each{ |t| print t.name }
#user.thing_ids.each{ |id| print id }
There are a large number of #user.things and I want to iterate through only their ids in batches, like with find_each. Is there a handy way to do this?
The goal is to:
not load the entire thing_ids array into memory at once
still only load arrays of thing_ids, and not instantiate a Thing for each id
Rails 5 introduced in_batches method, which yields a relation and uses pluck(primary_key) internally. And we can make use of the where_values_hash method of the relation in order to retrieve already-plucked ids:
#user.things.in_batches { |batch_rel| p batch_rel.where_values_hash['id'] }
Note that in_batches has order and limit restrictions similar to find_each.
This approach is a bit hacky since it depends on the internal implementation of in_batches and will fail if in_batches stops plucking ids in the future. A non-hacky method would be batch_rel.pluck(:id), but this runs the same pluck query twice.
You can try something like below, the each slice will take 4 elements at a time and them you can loop around the 4
#user.thing_ids.each_slice(4) do |batch|
batch.each do |id|
puts id
end
end
It is, unfortunately, not a one-liner or helper that will allow you to do this, so instead:
limit = 1000
offset = 0
loop do
batch = #user.things.limit(limit).offset(offset).pluck(:id)
batch.each { |id| puts id }
break if batch.count < limit
offset += limit
end
UPDATE Final EDIT:
I have updated my answer after reviewing your updated question (not sure why you would downvote after I backed up my answer with source code to prove it...but I don't hold grudges :)
Here is my solution, tested and working, so you can accept this as the answer if it pleases you.
Below, I have extended ActiveRecord::Relation, overriding the find_in_batches method to accept one additional option, :relation. When set to true, it will return the activerecord relation to your block, so you can then use your desired method 'pluck' to get only the ids of the target query.
#put this file in your lib directory:
#active_record_extension.rb
module ARAExtension
extend ActiveSupport::Concern
def find_in_batches(options = {})
options.assert_valid_keys(:start, :batch_size, :relation)
relation = self
start = options[:start]
batch_size = options[:batch_size] || 1000
unless block_given?
return to_enum(:find_in_batches, options) do
total = start ? where(table[primary_key].gteq(start)).size : size
(total - 1).div(batch_size) + 1
end
end
if logger && (arel.orders.present? || arel.taken.present?)
logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
end
relation = relation.reorder(batch_order).limit(batch_size)
records = start ? relation.where(table[primary_key].gteq(start)) : relation
records = records.to_a unless options[:relation]
while records.any?
records_size = records.size
primary_key_offset = records.last.id
raise "Primary key not included in the custom select clause" unless primary_key_offset
yield records
break if records_size < batch_size
records = relation.where(table[primary_key].gt(primary_key_offset))
records = records.to_a unless options[:relation]
end
end
end
ActiveRecord::Relation.send(:include, ARAExtension)
here is the initializer
#put this file in config/initializers directory:
#extensions.rb
require "active_record_extension"
Originally, this method forced a conversion of the relation to an array of activrecord objects and returned it to you. Now, I optionally allow you to return the query before the conversion to the array happens. Here is an example of how to use it:
#user.things.find_in_batches(:batch_size=>10, :relation=>true).each do |batch_query|
# do any kind of further querying/filtering/mapping that you want
# show that this is actually an activerecord relation, not an array of AR objects
puts batch_query.to_sql
# add more conditions to this query, this is just an example
batch_query = batch_query.where(:color=>"blue")
# pluck just the ids
puts batch_query.pluck(:id)
end
Ultimately, if you don't like any of the answers given on an SO post, you can roll-your-own solution. Consider only downvoting when an answer is either way off topic or not helpful in any way. We are all just trying to help. Downvoting an answer that has source code to prove it will only deter others from trying to help you.
Previous EDIT
In response to your comment (because my comment would not fit):
calling
thing_ids
internally uses
pluck
pluck internally uses
select_all
...which instantiates an activerecord Result
Previous 2nd EDIT:
This line of code within pluck returns an activerecord Result:
....
result = klass.connection.select_all(relation.arel, nil, bound_attributes)
...
I just stepped through the source code for you. Using select_all will save you some memory, but in the end, an activerecord Result was still created and mapped over even when you are using the pluck method.
I would use something like this:
User.things.find_each(batch_size: 1000).map(&:id)
This will give you an array of the ids.

Ruby on Rails: Update Attribute

Google is seriously failing me right now. All I need to do is update one attribute, setting a user to admin, from the Heroku rails console.
I can't find a single simple answer. What I've been trying is:
Record.update_attribute(:roles_mask, "1")
Where record is the correct record.
'undefined method 'update attribute''
I can't just type Record.roles_mask = 1?
EDIT.
I used Record, and I shouldn't have done that in the example. What I've done is exactly this:
ian = User.where(:id => '5')
ian.update_attribute(:roles_mask, '1')
Error: undefined method 'update_attributes'
The problem is that using the .where function creates a relation, rather than finding the record itself. Do this:
ian = User.find(5)
ian.update_attribute(:roles_mask, '1')
Or if you want to use .where then you could do this:
ian = User.where(:id => 5)
ian.first.update_attribute(:roles_mask, '1')
EDIT
See this answer for details about why this is happening.
To use update_attribute (or update_attributes), you need to call that from an instance and not the class.
rails c> rec = Record.find(1)
rails c> rec.update_attribute(:att, 'value')
rails c> rec.update_attributes(att: 'value', att2: 'value2')
I would think that should take care of your issue.
Where clause return a array and you are trying to make update query on array, that's why you got error.
you should try to find out first record
ian = User.where(:id => 1).first
or
ian = User.find(1)
or
ian = User.find_by_id(1)
now your update query will work.
ian.update_attribute(:roles_mask, '1')

Rails: Find and use the id number from a diff controller

I want to extract the id number of a unique record, that resides in a different controller as an integer, so I can save it as part of a new record in a new controller.
I can't seem to get the id to shed it's 'Array' attribute.
I've been using this:
class MessagesController < ApplicationController
def incoming
a = Group.where("name = ?", name).map { |n| n.id }
group_number = a.id
puts "#{group_number} is the number!"
end
output is always [2] or [3] or whatever.
adding this doesn't seem to cure it
group_as_int = group_number.to_i
Lastly, the reason I'm doing all this is to save that group number as a new record in a third controller, like this:
Subscriber.create(:group_id => group_number, :active => "1")
or
Subscriber.create(:group_id => group_as_int, :active => "1")
Of course, the create balks when I try to pass an array into the create function.
Thoughts?
You are trying to put business logic into the controller.
Try to refactor your methods and put them into your models instead.
Beside that you get the number in the following way:
group = Group.where("name = ?", name).first
group_number = group.id if group.present?
You might want to try .first to get the integer out of the array.
I will try to explain from your code what you did wrong.
The first line:
matching_group_ids = Group.where("name = ?", name).map { |n| n.id }
You called it a, but i prefer more verbose names. matching_group_ids now holds an array of id's. To get the first value of this array, the easiest solution is to just write
group_number = matching_group_ids[0]
or, more readable:
group_number = matching_group_ids.first
Mind you: you should test that the returned array is not empty.
Hope this helps.

Resources