23
loading...
This website collects cookies to deliver better user experience
class Teacher < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :teacher
end
.books
accessor method. But how? has_many :books
could make the associations and build out the methods for our Teacher class, I started by looking at the documentation before diving into the GitHub repo for Active Record to look at the source code to see what I could uncover. This turned out to be a somewhat ambitious project so I ultimately decided to split this up into two separate blog posts. In order to understand this, we'll need to spend some time uncovering what reflections are in ActiveRecord in this blog post before moving on to what the define_accessors
method is doing, and what mixins are in Ruby in the next. So let's get this duology off the ground!:has many
, will call a method that takes the following parameters:(name, scope = nil, **options, &extension)
has_many :books, -> { where(book_age > 10) }
:source
if you need to specify a different class name to prevent overlapping accessor methods and :dependent
if you wanted to set a way to destroy multiple associations when a #destroy
method is called. The syntax for creating an option mirrors how to pass in a scope parameter.has_many, belongs_to, and belongs_to_one
you have written have effectively been passed to this block of twelve lines of code twelve lines of code:def self.build(model, name, scope, options, &block)
if model.dangerous_attribute_method?(name)
raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
"this will conflict with a method #{name} already defined by Active Record. " \
"Please choose a different association name."
end
reflection = create_reflection(model, name, scope, options, &block)
define_accessors model, reflection
define_callbacks model, reflection
define_validations model, reflection
reflection
end
#build
method is fairly intuitive - it is a class level method which is called and is passed five arguments - the model, the name, the scope, the options and an optional block statement. So while we haven't seen the fact that the model is called in the documentation - when we go under the hood - we can see how the particular model is brought into the method that will create the association. The model will refer to our class model - so in our example above - it would be the Teacher. The name parameter refers to the name of the association. In our case, the :books symbol is being passed in here. The scope and options will line up with our corresponding understanding from starting in the ruby docs and and would be defined as nil in our above Teacher class. As a side note, as we work through the code you'll see examples of the options so let's put a pin it that for a moment for when we come back it. Finally, there is an optional block that can be passed in via the &block parameter - which we know would refer to any custom extensions we might want to add. reflection = create_reflection(model, name, scope, options, &block)
define_accessors model, reflection
def self.create_reflection(model, name, scope, options, &block)
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
validate_options(options)
extension = define_extensions(model, name, &block)
options[:extend] = [*options[:extend], extension] if extension
scope = build_scope(scope)
ActiveRecord::Reflection.create(macro, name, scope, options, model)
end
def self.define_extensions(model, name, &block)
if block_given?
extension_module_name = "#{name.to_s.camelize}AssociationExtension"
extension = Module.new(&block)
model.const_set(extension_module_name, extension)
end
end
book
won't work because it doesn't adhere to the camel cased creation happening throughout the codebase. This helps create our extension which is generated with a new Module class using our passed in block. If we were adding anything, our model would receive the extension model via the #const_set. Instead, as we won't meet the conditional requirement, we will simply jump down to the bottom of the method and hit our end to move move back and look at the options.options[:extend] = [*options[:extend], extension] if extension
scope = build_scope(scope)
def self.build_scope(scope)
if scope && scope.arity == 0
proc { instance_exec(&scope) }
else
scope
end
end
#arity
method is invoked on the scope variable (#arity is part of Ruby and returns, per the docs, "an indication of the number of arguments accepted by a method") it return that we have 0 arguments being accepted, then it uses a proc method to create a new variable which which, in turn, creates a hash that stores what the scope will be and returns it. While I do not entirely understand this, I believe what it does is if our scope
variable has no arguments when it is passed in (like ours would in this case), this line creates the properties necessary to prevent any errors as scope
is passed around later during the creation process. My reasoning is because if the scope is passed in and it clears this hurdle - that is - it possesses more than 0 arguments, it will instead simply be returned fully intact. So if the scope exists - leave it be. If it doesn't - let's 'create' it here - hence the build_scope method name. ActiveRecord::Reflection.create(macro, name, scope, options, model)
end
def create(macro, name, scope, options, ar)
reflection = reflection_class_for(macro).new(name, scope, options, ar)
options[:through] ? ThroughReflection.new(reflection) : reflection
end
:through
and define a way that teachers might connect to another model through the :books accessor, when we finish with the reflection_class_for
, we will simply return the reflection rather than creating a new ThroughReflection instance when we eventually hit the ternary statement. But - we're not there yet so let's not get too ahead of ourselves! Instead we now have a new definition to track down - the reflection_class_for
method. Well let's scroll down through this file and see ..def reflection_class_for(macro)
case macro
when :composed_of
AggregateReflection
when :has_many
HasManyReflection
when :has_one
HasOneReflection
when :belongs_to
BelongsToReflection
else
raise "Unsupported Macro: #{macro}"
end
end
end
:has_many
that started its journey oh so long ago is finally looking back at us! We won't hit that error at the end because we typed it correctly so let's scroll down the file to find the HasManyReflection below ...class HasManyReflection < AssociationReflection # :nodoc:
def macro; :has_many; end
def collection?; true; end
def association_class
if options[:through]
Associations::HasManyThroughAssociation
else
Associations::HasManyAssociation
end
end
end
has_many, through:
relationship - it will return the HasManyThroughAssocations module as opposed to the HasManyAssocation module. Otherwise it will define the method of #macro as has_many and create a method #collection?
which will be used later to let the builder know if this macro will require a collection (most likely a reference to an array, as we will see later) during creation. Neat. #destroy
is called on an object, how the teacher instance will be able access the array that contains its books, how to see the length of the specified association array, and how these pieces of information are updated 'behind the scenes' when books are added or removed from having an association with the teacher. def collection?; true; end
awhile ago? If this was false, there would be no need to create a collection. It also creates the setter and getter methods for collection IDs associated - in our case - to specific book foreign ID keys. We'll look at those methods in part two as they will more cleanly line up with the generation of our accessor methods.