Rails 4: select multiple attributes from a model instance - ruby-on-rails

How do I fetch multiple attributes from a model instance, e.g.
Resource.first.attributes(:foo, :bar, :baz)
# or
Resource.where(foo: 1).fetch(:foo, :bar, :baz)
rather than returning all the attributes and selecting them manually.

You will use the method slice.
Slice a hash to include only the given keys. Returns a hash containing the given keys.
Your code will be.
Resource.first.attributes.slice("foo", "bar", "baz")
# with .where
Resource.where(foo: 1).select("foo, bar, baz").map(&:attributes)

How about pluck:
Resource.where(something: 1).pluck(:foo, :bar, :baz)
Which translates to the following SQL:
SELECT "resources"."foo", "resources"."bar" FROM, "resources"."baz" FROM "resources"
And returns an array of the specified column values for each of the records in the relation:
[["anc", 1, "M2JjZGY"], ["Idk", 2, "ZTc1NjY"]]
http://guides.rubyonrails.org/active_record_querying.html#pluck
Couple of notes:
Multiple value pluck is supported starting from Rails 4, so if you're using Rails 3 it won't work.
pluck is defined on ActiveRelation, not on a single instnce.
If you want the result to be a hash of attribute name => value for each record you can zip the results by doing something like the following:
attrs = [:foo, :bar, :baz]
Resource.where(something: 1).pluck(*attrs).map{ |vals| attrs.zip(vals).to_h }

To Fetch Multiple has_one or belongs_to Relationships, Not Just Static Attributes.
To fetch multiple relationships, such as has_one or belongs_to, you can use slice directly on the instance, use values to obtain just the values and then manipulate them with a map or collect.
For example, to get the category and author of a book, you could do something like this:
book.slice( :category, :author ).values
#=> #<Category id: 1, name: "Science Fiction", ...>, #<Author id: 1, name: "Aldous Huxley", ...>
If you want to show the String values of these, you could use to_s, like:
book.slice( :category, :author ).values.map( &:to_s )
#=> [ "Science Fiction", "Aldous Huxley" ]
And you can further manipulate them using a join, like:
book.slice( :category, :author ).values.map( &:to_s ).join( "➝" )
#=> "Science Fiction ➝ Aldous Huxley"

Related

ActiveRecord group not returning hash with arrays of objects for values

I know this has been asked and answered a lot. I have two tables Foo and Bar.
class Foo < ApplicationRecord
belongs_to :bar
...
Foo has attributes of id and name and bar_id
and
class Bar < ApplicationRecord
has_many :foos
...
Bar has the attributes, id and name.
When I simply try Foo.group(:bar_id) I get #<Foo::ActiveRecord_Relation:0x3fdeac1cc274>
With Foo.group(:bar_id).count I get {5=>2, 1=>2} the keys being the bar_id and the values the count of how many have that id.
What I'm trying to do is, group Foo on Bar#name with an array of Foos as the values.
{
'name1' => [#<Foo:0x00007fbd5894f698 id:1, name: 'thing'...}, ...],
'name2' => [#<Foo:0x00017fbd5894f698 id:5, name: 'thing'...}, ...],
...
}
With Foo.joins(:bar).group('bars.name').count I am able to return {"name1"=>2, "name2"=>2} But not an array of the Foo models. I know it's because of the count. But without the count it simply returns #<Foo::ActiveRecord_Relation:0x3fdeac1cc274>
I see a lot of suggestions using Enumerable#group_by. I don't want to use an enumerable as I'm using ActiveRecord and as the records increase, it will drastically slow down the look up.
I've noticed that you're using PostgreSQL. Why not then use the json aggregation functions. It a bit differs from your desired result, but still contains the same information:
Bar
.joins(:foos)
.group("bars.name")
.pluck("bars.name", "json_agg(json_build_object('id', foos.id, 'name', foos.name))")
.to_h
The result is going to be:
{
'name1' => [{id:1, name: 'thing'}, ...],
'name2' => [{id:5, name: 'thing'}, ...],
...
}

Return name in ActiveRecord relation along with foreign key id

I have a Sub-Component model which can belong to other sub-components. My Model looks like this:
class SubComponent < ApplicationRecord
belongs_to :parent, class_name: "SubComponent", foreign_key: "parent_sub_component_id", optional: true
has_many :child_sub_components, class_name: "SubComponent", foreign_key: "parent_sub_component_id"
validates_presence_of :name
end
This model is fairly simple, it has a name field and a parent_sub_component_id which as the name suggests is an id of another SubComponent.
I'd like to generate a query that returns all of the SubComponents (with their id, name, and parent_sub_component_id) but also includes the actual name of it's parent_sub_component.
This seems like it should be pretty simple but for the life of me I can't figure out how to do it. I'd like for this query to be done in the database rather than doing an each loop in Ruby or something like that.
EDIT:
I'd like for the output to look something like this:
#<ActiveRecord::Relation [#<SubComponent id: 1, name: "Parent Sub", parent_sub_component_id: nil, parent_sub_component_name: nil created_at: "2017-07-07 00:29:37", updated_at: "2017-07-07 00:29:37">, #<SubComponent id: 2, name: "Child Sub", parent_sub_component_id: 1, parent_sub_component_name: "Parent Sub" created_at: "2017-07-07 00:29:37", updated_at: "2017-07-07 00:29:37">]>
You can do this efficiently using an each loop if you use includes:
SubComponent.all.includes(:parent).each do |comp|
comp.parent.name # this gives you the name of the parent
end
What includes does is it pre-fetches the specified association. That is, ActiveRecord will query all subcomponents, and then in a single query also pull down all the parents of those subcomponents. When you subsequently access comp.parent in the loop, the associated parent will already be loaded, so this will not result in a so-called N+1 query.
The queries that AR will generate for you automatically will look something like this:
SELECT `subcomponents`.* FROM `subcomponents`
SELECT `subcomponents`.* FROM `subcomponents` WHERE `subcomponents`.`id` IN (1, 3, 9, 14)
If you need to use the name of the parent in a where condition, includes will not work and you will have to use joins instead to actually generate an SQL JOIN.
This is untested, but should get you started in the right direction, you can do this in Arel by doing something like
def self.execute_query
parent_table = Arel::Table.new(:sub_component).alias
child_table = Arel::Table.new(:sub_component)
child_table.join(parent_table, Arel::Nodes::OuterJoin).on(child_table[:parent_sub_component_id].eq(parent_table[:id]).project(child_table[:id], child_table[:name], parent_table[:id], parent_table[:name])
end
This results in a query like
SELECT "sub_component"."id", "sub_component"."name", "sub_component_2"."id", "sub_component_2"."name" FROM "sub_component" LEFT OUTER JOIN "sub_component" "sub_component_2" ON "sub_component"."parent_sub_component_id" = "sub_component_2"."id"
this is just off the top of my head by looking at Rails/Arel and probably needs a some work, but the query looks about what I would expect and this should get you going.

Rails - Empty serialized field on create

How should I create a record with a serialized array field to a string.
Like this?
ExerciseStudent.create(exercise_id: e.id, student_id: self.id, load: "[]", repetition: "[]")
Or simple like this?
ExerciseStudent.create(exercise_id: e.id, student_id: self.id, load: "", repetition: "")
Later when filled with data I want to look like this:
exercise.load == "[15, 20, 30]"
My model:
class ExerciseStudent < ActiveRecord::Base
serialize :load, Array
serialize :repetition, Array
belongs_to :exercise
belongs_to :student
end
Instead of trying to represent the value as a String instance, just go all in and use Arrays. Since you've told Rails to serialize the attribute it'll handle converting it from an Array to YAML on save and vice versa on load ("load" as in from the database and into the object in memory).
exercise.load = [15, 20, 30]
And:
ExerciseStudent.create(exercise_id: e.id, student_id: self.id, load: [], repetition: [])

MongoDB Bulk Insert Performance

I have the following code in my rails app.
module UserItem
class Rating
include MongoMapper::Document
key :user_id, Integer, :required => true
key :item_id, Integer, :required => true
key :rating, Float, :required => true
end
end
And I have about 10K users and 10K items and i need to store rating of each user for each item, which is about 10^8 records. I have computed the values of 10^8 records into an array as follows
ratings = [
{user_id: 1, item_id: 1, rating: 1.5},
{user_id: 1, item_id: 2, rating: 3.5},
... and so on 10^8 records
]
Now, I need to insert all these 10^8 records computed into mongo. I tried with
UserItem::Rating.collection.insert(ratings)
and
UserItem::Rating.create(ratings)
But it takes hours together to insert the 10^8 records into mongo. Is there any better/efficient way to insert records into mongo?
Context: I am using it more like a cache store which stores all rating values. When I display list of items, I will just read from this cache and display the rating provided by the user alongside each item.
Any help is much appreciated!
One approach is to store one document per user, with a ratings field that is a hash of item ids to users, for example
class UserRating
include MongoMapper::Document
key :ratings
key :user_id
end
UserRating.create(:user_id => 1, :ratings => {"1" => 4, "2" => 3})
You have to use string keys for the hash. This approach doesn't make it easy to retrieve all the ratings for a given document - if you do that a lot it might be easier to store a document per item instead. It's also probably not very efficient if you only ever need a small proportion of a user's ratings at a time.
Obviously you can combine this with other approaches to increasing write throughput, such as batching your inserts or sharding your database.

Case-insensitive search in Rails model

My product model contains some items
Product.first
=> #<Product id: 10, name: "Blue jeans" >
I'm now importing some product parameters from another dataset, but there are inconsistencies in the spelling of the names. For instance, in the other dataset, Blue jeans could be spelled Blue Jeans.
I wanted to Product.find_or_create_by_name("Blue Jeans"), but this will create a new product, almost identical to the first. What are my options if I want to find and compare the lowercased name.
Performance issues is not really important here: There are only 100-200 products, and I want to run this as a migration that imports the data.
Any ideas?
You'll probably have to be more verbose here
name = "Blue Jeans"
model = Product.where('lower(name) = ?', name.downcase).first
model ||= Product.create(:name => name)
This is a complete setup in Rails, for my own reference. I'm happy if it helps you too.
the query:
Product.where("lower(name) = ?", name.downcase).first
the validator:
validates :name, presence: true, uniqueness: {case_sensitive: false}
the index (answer from Case-insensitive unique index in Rails/ActiveRecord?):
execute "CREATE UNIQUE INDEX index_products_on_lower_name ON products USING btree (lower(name));"
I wish there was a more beautiful way to do the first and the last, but then again, Rails and ActiveRecord is open source, we shouldn't complain - we can implement it ourselves and send pull request.
If you are using Postegres and Rails 4+, then you have the option of using column type CITEXT, which will allow case insensitive queries without having to write out the query logic.
The migration:
def change
enable_extension :citext
change_column :products, :name, :citext
add_index :products, :name, unique: true # If you want to index the product names
end
And to test it out you should expect the following:
Product.create! name: 'jOgGers'
=> #<Product id: 1, name: "jOgGers">
Product.find_by(name: 'joggers')
=> #<Product id: 1, name: "jOgGers">
Product.find_by(name: 'JOGGERS')
=> #<Product id: 1, name: "jOgGers">
You might want to use the following:
validates_uniqueness_of :name, :case_sensitive => false
Please note that by default the setting is :case_sensitive => false, so you don't even need to write this option if you haven't changed other ways.
Find more at:
http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of
Several comments refer to Arel, without providing an example.
Here is an Arel example of a case-insensitive search:
Product.where(Product.arel_table[:name].matches('Blue Jeans'))
The advantage of this type of solution is that it is database-agnostic - it will use the correct SQL commands for your current adapter (matches will use ILIKE for Postgres, and LIKE for everything else).
In postgres:
user = User.find(:first, :conditions => ['username ~* ?', "regedarek"])
Quoting from the SQLite documentation:
Any other character matches itself or
its lower/upper case equivalent (i.e.
case-insensitive matching)
...which I didn't know.But it works:
sqlite> create table products (name string);
sqlite> insert into products values ("Blue jeans");
sqlite> select * from products where name = 'Blue Jeans';
sqlite> select * from products where name like 'Blue Jeans';
Blue jeans
So you could do something like this:
name = 'Blue jeans'
if prod = Product.find(:conditions => ['name LIKE ?', name])
# update product or whatever
else
prod = Product.create(:name => name)
end
Not #find_or_create, I know, and it may not be very cross-database friendly, but worth looking at?
Similar to Andrews which is #1:
Something that worked for me is:
name = "Blue Jeans"
Product.find_by("lower(name) = ?", name.downcase)
This eliminates the need to do a #where and #first in the same query. Hope this helps!
Another approach that no one has mentioned is to add case insensitive finders into ActiveRecord::Base. Details can be found here. The advantage of this approach is that you don't have to modify every model, and you don't have to add the lower() clause to all your case insensitive queries, you just use a different finder method instead.
Upper and lower case letters differ only by a single bit. The most efficient way to search them is to ignore this bit, not to convert lower or upper, etc. See keywords COLLATION for MSSQL, see NLS_SORT=BINARY_CI if using Oracle, etc.
Find_or_create is now deprecated, you should use an AR Relation instead plus first_or_create, like so:
TombolaEntry.where("lower(name) = ?", self.name.downcase).first_or_create(name: self.name)
This will return the first matched object, or create one for you if none exists.
An alternative can be
c = Product.find_by("LOWER(name)= ?", name.downcase)
Case-insensitive searching comes built-in with Rails. It accounts for differences in database implementations. Use either the built-in Arel library, or a gem like Squeel.
There are lots of great answers here, particularly #oma's. But one other thing you could try is to use custom column serialization. If you don't mind everything being stored lowercase in your db then you could create:
# lib/serializers/downcasing_string_serializer.rb
module Serializers
class DowncasingStringSerializer
def self.load(value)
value
end
def self.dump(value)
value.downcase
end
end
end
Then in your model:
# app/models/my_model.rb
serialize :name, Serializers::DowncasingStringSerializer
validates_uniqueness_of :name, :case_sensitive => false
The benefit of this approach is that you can still use all the regular finders (including find_or_create_by) without using custom scopes, functions, or having lower(name) = ? in your queries.
The downside is that you lose casing information in the database.
You can also use scopes like this below and put them in a concern and include in models you may need them:
scope :ci_find, lambda { |column, value| where("lower(#{column}) = ?", value.downcase).first }
Then use like this:
Model.ci_find('column', 'value')
If you're using postgres (probably others), I like this solution.
Product.find_by("name ilike 'bLue JEaNS'")
I like this better for a couple reasons.
Clearer connection to database action -> you can just copy paste that into where ...
If you choose to add a wildard %, it's straightforward.
Assuming that you use mysql, you could use fields that are not case sensitive: http://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html
user = Product.where(email: /^#{email}$/i).first
Some people show using LIKE or ILIKE, but those allow regex searches. Also you don't need to downcase in Ruby. You can let the database do it for you. I think it may be faster. Also first_or_create can be used after where.
# app/models/product.rb
class Product < ActiveRecord::Base
# case insensitive name
def self.ci_name(text)
where("lower(name) = lower(?)", text)
end
end
# first_or_create can be used after a where clause
Product.ci_name("Blue Jeans").first_or_create
# Product Load (1.2ms) SELECT "products".* FROM "products" WHERE (lower(name) = lower('Blue Jeans')) ORDER BY "products"."id" ASC LIMIT 1
# => #<Product id: 1, name: "Blue jeans", created_at: "2016-03-27 01:41:45", updated_at: "2016-03-27 01:41:45">
You can use like this in model
scope :matching, lambda { |search, *cols|
where cols.flatten.map{|col| User.arel_table[col].matches("%#{search}%") }.inject(:or)
}
and use wherever you like this
User.matching(params[:search], :mobile_number, :name, :email)
You can pass multiple column for search
for single column search you can use like this
User.where(User.arel_table[:column].matches("%#{search}%"))
So far, I made a solution using Ruby. Place this inside the Product model:
#return first of matching products (id only to minimize memory consumption)
def self.custom_find_by_name(product_name)
##product_names ||= Product.all(:select=>'id, name')
##product_names.select{|p| p.name.downcase == product_name.downcase}.first
end
#remember a way to flush finder cache in case you run this from console
def self.flush_custom_finder_cache!
##product_names = nil
end
This will give me the first product where names match. Or nil.
>> Product.create(:name => "Blue jeans")
=> #<Product id: 303, name: "Blue jeans">
>> Product.custom_find_by_name("Blue Jeans")
=> nil
>> Product.flush_custom_finder_cache!
=> nil
>> Product.custom_find_by_name("Blue Jeans")
=> #<Product id: 303, name: "Blue jeans">
>>
>> #SUCCESS! I found you :)

Resources