My goal is to implement something like "has_many" in Rails' ActiveRecord. I want to understand more about how the classes/objects are loaded.
What is happening when I use "has_many" in the following?
class Widget < MiniRecord
has_many :trinkets
end
Let's say that this is the implementation of "MiniRecord":
class MiniRecord
class << self
def has_many associated_model
#associations[:has_many] << associated_model_sym
end
end
def list_associations
do_stuff(#associations) # Where do I instantiate #associations?
end
end
I've gotten it to the point where it works, but I have some messy code that I'd like to trim down.
I want to understand how this works, but specifically, where should I instantiate #associations?
I want to be able to "declare" attributes in subclasses of MiniRecord that is defined when the class is defined (i.e. a static variable), and then act on these attributes later.
When you extend MiniRecord, say MyRecord, you got a class hierarchy figure something like the following (#SomeClass is eigenclass or singleton class of SomeClass):
Class
|
MiniRecord --class--> #MiniRecord
| |
MyRecord --class--> #MyRecord
Where the list_associations method is defined on MiniRecord and has_many is defined on #MiniRecord.
When you call has_many in MyRecord's class body, it will first look up the method in #MyRecord, which will fail. Then it will look up in #MiniRecord and found the definition. If it fails somehow, it will look in Class for the definition. That's how this approach works.
For the question
specifically, where should I instantiate #associations?
You have two choice:
initialize when used
def has_many associated_model
#associations ||= Hash.new{|h, k| h[k] = [] }
#associations[:has_many] << associated_model_sym
end
initialize when the descendant class get created
class MiniRecord
def self.inherited klass
klass.instance_variable_set(:#associations, Hash.new{|h, k| h[k] = [] }
end
end
Note that you cannot access #associations in instance method list_associations directly, because it's in different scope. You need to expose #associations on MiniRecord and then get the value from there.
class MiniRecord
class << self
def has_many associated_model
#associations[:has_many] << associated_model
end
attr_reader :associations
end
def self.inherited klass
klass.instance_variable_set(:#associations, Hash.new{|h, k| h[k] = [] })
end
def list_associations
do_stuff(self.class.associations) # Where do I instantiate #associations?
end
def do_stuff sth
p sth
end
end
class MyRecord < MiniRecord
has_many :thrinks
end
r = MyRecord.new
r.list_associations
p MyRecord.associations
Related
Having:
class Foo < ApplicationRecord
def self.to_csv
# irrelevant
end
end
Rails allow me to do:
Foo.all.to_csv
But how would I access the collection that received the method call inside to_csv? (all in this case)
This may seem counter intuitive but you can use #all
For example:
class Foo < ActiveRecord::Base
def self.to_csv
all.map(&:convert_to_csv)
end
end
Not only this will work with Foo.all.to_csv but also with Foo.where(...).to_csv
If you look at the source of #all inside ActiveRecord:
def all
if current_scope
current_scope.clone
else
default_scoped
end
end
This means if you have defined a scope with where or limit it will respect it. Or if you're grabbing all records it will just use default_scoped
I have a non activerecord rails model:
class Document
attr_accessor :a, :b
include ActiveModel::Model
def find(id)
initialize_parameters(id)
end
def save
...
end
def update
...
end
private
def initialize_parameters(id)
#a = 1
#b = 2
end
end
In order to find the Document, I can use:
Document.new.find(3)
So, to get it directly I changed the find method to
def self.find(id)
initialize_parameters(id)
end
And I get the following error when I run
Document.find(3)
undefined method `initialize_parameters' for Document:Class
How can I make this work?
You can't access an instance method from a class method that way, to do it you should instantiate the class you're working in (self) and access that method, like:
def self.find(id)
self.new.initialize_parameters(id)
end
But as you're defining initialize_parameters as a private method, then the way to access to it is by using send, to reach that method and pass the id argument:
def self.find(id)
self.new.send(:initialize_parameters, id)
end
private
def initialize_parameters(id)
#a = 1
#b = 2
end
Or just by updating initialize_parameters as a class method, and removing the private keyword, that wouldn't be needed anymore.
This:
class Document
attr_accessor :a, :b
def self.find(id)
initialize_parameters(id)
end
end
Is not trying to "access class method from instance method" as your title states. It is trying to access a (non-existent) class method from a class method.
Everything Sebastian said is spot on.
However, I guess I would ask: 'What are you really trying to do?' Why do you have initialize_parameters when ruby already gives you initialize that you can override to your heart's content? IMO, it should look something more like:
class Document
attr_accessor :a, :b, :id
class << self
def find(id)
new(id).find
end
end
def initialize(id)
#a = 1
#b = 2
#id = id
end
def find
# if you want you can:
call_a_private_method
end
private
def call_a_private_method
puts id
end
end
I'm trying to access variables defined in class One, through inheritance, in class Two. I can't seem to find the right way of going about it - it seems to work for methods:
class One
class << self
def test
puts "I'm a method from class one"
end
end
end
end
And as a new object the variable is accessible:
class Two < One
test
end
#=> I'm a method from class one
class Test
attr_accessor :a
def initialize
#a = "hi"
end
end
Test.new.a
#=> "hi"
But I'm trying to do something like:
class One
class << self
a = "hi"
end
end
class Two < One
a
end
#=> NameError: undefined local variable or method `a' for Two:Class
For now I'm using class variables, but I'm sure there's a better way:
class One
##a = "hi"
end
class Two < One
##a
end
#=> "hi"
local and class instance variables wouldn't be accessible through inheritance in Ruby.
Limosine is an example of a class inheriting, a variable (brand) and a method, to_s
class Car
def initialize(brand)
#brand = brand
end
def to_s
"(##brand, ##model)"
end
end
class Limosine < Car
def initialize(brand, model)
super(brand)
#model = model
end
end
Use:
puts Merc.new("Mercedes", "Maybach")to_s
I'm trying to do something like:
account.users << User.new
But I need users to be a method on an account. So I've tried things like:
def users<<(obj)
But I've had no luck with that. Is this even possible to do in Ruby? I would assume so because the ActiveRecord relationships seem to work this way in Rails.
Check this answer: Rails: Overriding ActiveRecord association method
[this code is completely from the other answer, here for future searchers]
has_many :tags, :through => :taggings, :order => :name do
def << (value)
"overriden" #your code here
end
end
It seems like you might not be describing your actual problem, but to answer your question -- yes you can override the << operator:
class Foo
def <<(x)
puts "hi! #{x}"
end
end
f = Foo.new
=> #<Foo:0x00000009b389f0>
> f << "there"
hi! there
I assume you have a model like this:
class Account < ActiveRecord::Base
has_and_belongs_to_many :users
end
To override Account#users<<, you need to define it in a block that you pass to has_and_belongs_to_many:
class Account < ActiveRecord::Base
has_and_belongs_to_many :users do
def <<(user)
# ...
end
end
end
You can access the appropriate Account object by referring to proxy_association.owner:
def <<(user)
account = proxy_association.owner
end
To call the original Account#users<<, call Account#users.concat:
def <<(user)
account = proxy_association.owner
# user = do_something(user)
account.users.concat(user)
end
For more details, see this page: Association extensions - ActiveRecord
In this case it's the << of you class of you User. So can be an Array or a AssociationProxy.
The must simplest is create a new method to do what you want.
You can override the method by instance instead.
account.users.instance_eval do
def <<(x)
put 'add'
end
end
account.users << User.new
# add
But you need do that all the time before you add by <<
users would return an object that has overridden << operator like Array, IO, String, or any type you create. You override like this:
class SomeType
def <<(obj)
puts "Appending #{obj}"
end
end
If you are trying to perform an action upon adding an User to the users collection, you can use association callbacks instead of over-riding <<(as there are many ways to add an object to an association).
class Account
has_many :users, :after_add => :on_user_add
def on_user_add(user)
p "Added user : #{user.name} to the account: #{name}"
end
end
I am trying to do a custom active record macro. But it right now seems impossible set an instance variable from within it's block.. here is what i am trying to do.
module ActiveRecord
class Base
def self.included(base)
base.class.send(:define_method, :my_macro) do |args|
# instance_variable_set for the model instance that has called this
# macro using args
end
end
end
end
i have tried class_eval, instance_eval.. but nothing seems to work or i don't how to use them.
Thanks in advance.
Edit: Let me try to explain better. I have a class method. An instance of the class calls this method. Now, this class method should instruct the instance to set an instance variable for itself.
Edit- this is how i want o use the macro
class MyModel < ActiveRecord::Base
my_macro(*args)
def after_initialize
# use the value set in the macro as #instance variable
end
end
Is this what you are thinking of:
class DynamicAdd
def add_method
self.class_eval do
attr_accessor :some_method
end
end
end
You can then do the following:
k = DynamicAdd.new
k.some_method = "hi"
should result in an undefined method error.
But,
k = DynamicAdd.new
k.add_method
k.some_method = "hi"
should work.
You can use this same format to define other types of methods besides attr_accessors as well:
class DynamicAdd
def add_method
self.class_eval do
def some_method
return "hi"
end
end
end
end
Hm.. Isn't included() a Module method? I don't think you can use that in a class like you have written. If you want to create a class method you can do
class Base
def self.my_method
end
or
class Base
class << self
def my_method
end
end
If all you want to do is to add an instance variable to an existing object, then you can use #instance_variable_set
class Base
class << self
def my_method(instance_of_base, value)
instance_of_base.instance_variable_set "#x", value
end
end
end
a = Base.new
a.class.send(:my_method, *[a,4])