How does ActiveRecord::Relation class act like array?
For example,
User.all
=> #<ActiveRecord::Relation [#<User id: 1, name: "Alex", nickname: "leha", created_at: "2017-05-05 12:36:31", updated_at: "2017-05-05 12:36:31">]>
It returns ActiveRecord::Relation which acts like Array.
If I create my own class MyClass
class MyClass
attr_accessor :relation
def initialize(options)
#relation = options
end
end
m = MyClass.new [1,2,3]
=> #<MyClass:0x007ffa3f9ab730 #relation=[1, 2, 3]>
The question is how to make class like ActiveRecord::Relation?
Because includes the module Enumerable, that enables a class to be sorted. Most of the methods that Arrays implements came from that specific module. So if you want to create a class that acts like an Array you should implement Enumerable.
As you see in the following code from Active Record, you can notice when it is included in the class:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation.rb#L15
References:
https://ruby-doc.org/core-2.4.1/Enumerable.html
https://ruby-doc.org/core-2.4.1/Array.html#class-Array-label-Iterating+over+Arrays
Related
I have an active record model that has a column called configuration of type text. That column is serialized with a custom class, like so:
class MyModel < ApplicationRecord
serialize :configuration, MySerializer
end
The class MySerializer has the following class methods:
def dump(configuration)
configuration.to_json if configuration
end
def load(configuration)
obj = new
obj.json_hash = JSON.parse(configuration) if configuration.present?
obj
end
This instantiates an instance of the class MySerializer with the attr accessor json_hash.
Now, here's the problem, I'm doing:
MyModel.create(configuration: {"key" => 1})
And once I do MyModel.first, i get this:
...
configuration:
#<MySerializer:0x00000007faa558
#json_hash={"json_hash"=>{"key" => 1}
I was expecting getting something like:
#json_hash = {"key" => 1}
Any idea why I'd get the repeated key json_hash inside the attr accessor #json_hash ?
Thanks!
Why do you want to use MySerializer class?
Instead you can simply use as below:
serialize :configuration, Hash
Now do,
MyModel.create(configuration: {"key" => 1})
And try
MyModel.first
Suppose I have a class Article, such that:
class Article
attr_accessor :title, :author
def initialize(title, author)
#title = title
#author= author
end
end
Also, variable atrib is a String containing the name of an attribute. How could I turn this string into a variable to use as a getter?
a = Article.new
atrib='title'
puts a.eval(atrib) # <---- I want to do this
EXTENDED
Suppose I now have an Array of articles, and I want to sort them by title. Is there a way to do the compact version using & as in:
col = Article[0..10]
sorted_one = col.sort_by{|a| a.try('title') } #This works
sorted_two = col.sort_by(&:try('title')) #This does not work
You can use either send or instance_variable_get:
a = Article.new 'Asdf', 'Coco'
a.pubic_send(:title) # (Recommended) Tries to call a public method named 'title'. Can raise NoMethodError
=> "Asdf"
# If at rails like your case:
a.try :title # Tries to call 'title' method, returns `nil` if the receiver is `nil` or it does not respond to method 'title'
=> "Asdf"
a.send(:title) # Same, but will work even if the method is private/protected
=> "Asdf"
a.instance_variable_get :#title # Looks for an instance variable, returns nil if one doesn't exist
=> "Asdf"
Shot answer to your extended question: no. The &:symbol shortcut for procs relies on Symbol#to_proc method. So to enable that behavior you'd need to redifine that method on the Symbol class:
class Symbol
def to_proc
->(x) { x.instance_eval(self.to_s) }
end
end
[1,2,3].map(&:"to_s.to_i * 10")
=> [10, 20, 30]
ActiveRecord instances have an attributes hash:
a = Article.new(title: 'foo')
#=> <#Article id: nil, title: "foo">
atrib = 'title'
a.attributes[atrib]
#=> "foo"
You can use order to get sorted objects from your database:
Article.order('title').first(10)
#=> array of first 10 articles ordered by title
I have a descendants method in my Question model to return all objects that inherit from it.
class Question < ActiveRecord::Base
class << self
def descendants
ObjectSpace.each_object(Class).select do |klass|
klass < self
end
end
end
end
When I call Question.descendants I get back an array w/ a single object
[MultipleChoice(id: integer, text: text, scored: boolean, required: boolean, type: string, questionnaire_id: integer, created_at: datetime, updated_at: datetime)]
The problem is that when I call Question.descendants.first.class I get back Class instead of the expected MultipleChoice.
Why is this happening?
The thing is, that you already have a class in the array (the MultipleChoice class). When you ask Question.descendants.first you get that MultipleChoice class.
However, you are asking for Question.descendants.first**.class**. And the class of MultipleChoice is Class.
Getting Class as the class of MultipleChoice is perfectly OK. Have a look at the ruby metamodel as a reference:
image source: http://sermoa.wordpress.com/2011/06/19/ruby-classes-and-superclasses/
You have MultipleChoice class instead of instance in your array returned by descendants method. This is because you used ObjectSpace.each_object with Class parameter, which returns classes, since their class is Class.
[MultipleChoice(id: integer, text: text, scored: boolean, required: boolean, type: string, questionnaire_id: integer, created_at: datetime, updated_at: datetime)]
This is not an array of single object. This is an array in which you have something like [MultipleChoice]. And when you try MultipleChoice.class it will return Class.
There is some issue in your code that creates Question.descendants
I would like to create a virtual attribute that will always be included when you do model_instance.inspect. I understand that attr_reader will give me the same thing as just defining an instance method, but I would like this attribute to be part of the object's "make up"
How can I accomplish this?
Thanks!
UPDATE
Here is what is not working in more detail:
class Product < ActiveRecord::Base
attr_reader :less_secure_asset_url
def less_secure_asset_url
self.asset.url
end
end
>> p = Product.find(:all)[1]
=> #<Product id: 49, asset_file_name: "Etrade_Trade_Conf_Request.docx", asset_content_type: "application/vnd.openxmlformats-officedocument.wordp...", asset_file_size: 38152, asset_updated_at: "2010-05-04 17:45:46", created_at: "2010-05-04 17:45:46", updated_at: "2010-05-04 17:45:46", owner_id: 345, product_type_id: 1>
As you can see, when I use the console it returns no "less_secure_asset_url" attribute
Your attribute is in there, even if it doesn't show up. Rails overrides the definition of the inspect method of Object in ActiveRecord::Base to something like:
def inspect
attributes_as_nice_string = self.class.column_names.collect { |name|
if has_attribute?(name) || new_record?
"#{name}: #{attribute_for_inspect(name)}"
end
}.compact.join(", ")
"#<#{self.class} #{attributes_as_nice_string}>"
end
Basically, if it's not a column_name, it's not going to show up. I haven't tried this, but you might be able to call something like p.as(Object).inspect to get to the super classes inspect method. You have to require 'facets' to get as. See docs here.
How do you persist a derived attribute which depends on the value of id in rails? The snippet below seems to work-- Is there a better rails way?
class Model < ActiveRecord::Base
....
def save
super
#derived_attr column exists in DB
self.derived_attr = compute_attr(self.id)
super
end
end
Callbacks are provided so you should never have to override save. The before_save call in the following code is functionally equivalent to all the code in the question.
I've made set_virtual_attr public so that it can be calculated as needed.
class Model < ActiveRecord::Base
...
# this one line is functionally equivalent to the code in the OP.
before_save :set_virtual_attr
attr_reader :virtual_attr
def set_virtual_attr
self.virtual_attr = compute_attr(self.id)
end
private
def compute_attr
...
end
end
I think the more accepted way to do this would be to provide a custom setter for the virtual attribute and then provide an after_create hook to set the value after the record is created.
The following code should do what you want.
class Virt < ActiveRecord::Base
def after_create()
self.virtual_attr = nil # Set it to anything just to invoke the setter
save # Saving will not invoke this callback again as the record exists
# Do NOT try this in after_save or you will get a Stack Overflow
end
def virtual_attr=(value)
write_attribute(:virtual_attr, "ID: #{self.id} #{value}")
end
end
Running this in the console shows the following
v=Virt.new
=> #<Virt id: nil, virtual_attr: nil, created_at: nil, updated_at: nil>
>> v.save
=> true
>> v
=> #<Virt id: 8, virtual_attr: "ID: 8 ", created_at: "2009-12-23 09:25:17",
updated_at: "2009-12-23 09:25:17">
>> Virt.last
=> #<Virt id: 8, virtual_attr: "ID: 8 ", created_at: "2009-12-23 09:25:17",
updated_at: "2009-12-23 09:25:17">