If you’ve used the Rails framework, you will probably recognise this:
class Comment < ApplicationRecord
belongs_to :article
end
This snippet of code implies three things:
1) We have a table of comments.
2) We have a table of articles.
3) Each comment is related to an article by some ID.
Rails users will take for granted that if they have an instance of the Comment class, they will be able to execute some_comment.article
to obtain the article that the comment is related to.
This post will give you an extremely simplified look at how something like Rails’ ActiveRecord relations can be achieved. First, some groundwork.
Modules
Modules in Ruby can be used to extend the behaviour of a class, and there are three ways in which they can do this: include
, prepend
, and extend
. The difference between the three? Where they fall in the method lookup chain.
class MyClass
prepend PrependingModule
include IncludingModule
extend ExtendingModule
end
In the above example:
- Methods from
PrependingModule
will be created as instance methods and override instance methods fromMyClass
. - Methods from
IncludingModule
will be created as instance methods but not override methods fromMyClass
. - Methods from
ExtendingModule
will be added as class methods onMyClass
.
We can do fun things with extend
.
Executing Code During Interpretation Time
module Ownable
def belongs_to(owner)
puts "I belong to #{owner}!"
end
end
class Item
extend Ownable
belongs_to :overlord
end
In the above code, we’re just defining a module and a class. No instance of the class is ever created. However, when we execute just this code in an IRB session, you will see “I belong to overlord!” as the output. Why? Because the code we write while defining a class is executed as that class definition is being interpreted.
What if we re-write our module to define a method using Ruby’s define_method?
module Ownable
def belongs_to(owner)
define_method(owner.to_sym) do
puts self.object_id
end
end
end
Whatever we passed as the argument to belongs_to
will become a method on instances of our Item
class.
our_item = Item.first
our_item.overlord
# => 70368441222580
Excellent. You may have heard this term before, but this is “metaprogramming”. Writing code that writes code. You just metaprogrammed.
Tying It Together
You might also notice that we’re getting closer to the behaviour that we would expect from Rails.
So let’s say we have our Item
class, and we’re making a videogame, so we’re going to say that our item belongs to a player.
class Item
extend Ownable
belongs_to :player
end
Our Rails-like system could make some assumptions about this.
1) There is a table in the database called players
.
2) There is a column in our items
table called player_id
.
3) The player model is represented by the class Player
.
Let’s return to our module and tweak it based on these assumptions.
module Ownable
def belongs_to(owner)
define_method(owner.to_sym) do
# We need to get `Player` out of `:player`
klass = owner.to_s.capitalize.constantize
# We need to turn `:player` into `:player_id`
foreign_key = "#{owner}_id".to_sym
# We need to execute the actual query
klass.find_by(id: self.send(foreign_key))
# SELECT * FROM players WHERE id = :player_id LIMIT 1
end
end
end
class Item
extend Ownable
belongs_to :player
end
my_item = Item.first
my_item.player
# SELECT * FROM players WHERE id = 1 LIMIT 1
# => #<Player id: 12>
Neat.
Top comments (0)