What is the difference between Ruby's Hash
and ActiveSupport
's
HashWithIndifferentAccess
?
The Hash
class in Ruby's core library retrieves values by doing a standard ==
comparison on the keys. This means that a value stored for a Symbol
key (e.g. :my_value)
cannot be retrieved using the equivalent String
(e.g. 'my_value'). On the other hand,
HashWithIndifferentAccess
treats Symbol
keys and String
keys as equivalent so that
the following would work:
h = HashWithIndifferentAccess.new
h[:my_value] = 'foo'
h['my_value'] #=> will return "foo"
What's the problem with the following controller code? What would the consequence of leaving this code in a production app? How would you fix it?
class MyController < ApplicationController
def update_options
available_option_keys = [:first_option, :second_option, :third_option]
all_keys = params.keys.map(&:to_sym)
set_option_keys = all_keys & available_option_keys
set_option_keys.each do |key|
options[key] = params[key]
end
end
end
Because Symbol
objects in Ruby are not garbage collected, it is dangerous to convert
user supplied parameters to symbols. An attacker could send a series of requests with
random keys that would be turned into symbols, quickly exhausting your server's available
memory and taking down your site.
There are two ways that this could could be fixed. The first would be to use slice
to
eliminate values from the params
hash that are not valid option keys. This would
look something like:
params.slice(available_option_keys).each do |key|
options[key] = params[key]
end
The other, some would argue better, option would to simply be to use String
keys for
your options. Unless you have an extremely large number of possible option keys, you won't
actually save that much memory by using Symbol
keys instead.
What is the problem with the following controller code? How would you fix it?
class CommentsController < ApplicationController
def users_comments
posts = Post.all
comments = posts.map(&:comments).flatten
@user_comments = comments.filter do |comment|
comment.author.username == params[:username]
end
end
end
This is a classic example of an "n+1" bug. The first line will retrieve all of the Post
objects from the database, but then the very next line will make an additional request for
each Post
to retrieve the corresponding Comment
objects. To make matters worse, this
code is then making even more database requests in order to retrieve the Author
of
each Comment
.
This can all be avoided by changing the first line in the method to:
posts = Post.includes(comments: [:author]).all
This will tell ActiveRecord to perform a database join between the tables for Post
, Comment
,
and Author
, reducing the number of database requests to just one.
What is CSRF? How does Rails protect against CSRF?
CSRF stands for Cross-Site Request Forgery. This is a form of an attack where the attacker
submits a form on your behalf to a different website, potentially causing damage or
revealing sensitive information. Since browsers will automatically include cookies for a
domain on a request, if you were recently logged in to the target site, the attacker's
request will appear to come from a logged-in user (as your session cookie will be sent
with the POST
request).
In order to protect against CSRF attacks, you can add protect_from_forgery
to your
ApplicationController
. This will then cause Rails to require that a CSRF token is
present before accepting any POST
, PUT
, or DELETE
requests. The CSRF token is
included as a hidden field in every form created using Rails' form builders. It is also
included as a header in GET
requests so that other, non-form-based mechanisms for
sending a POST
can also use it. Attackers are prevented from stealing the CSRF token by
browsers' "same origin" policy.
How would you define a Person
model so that any Person
can be assigned as the parent
of another Person
(as demonstrated in the Rails console below)? What columns would you
need to define in the migration creating the table for Person
?
irb(main):001:0> john = Person.create(name: "John")
irb(main):002:0> jim = Person.create(name: "Jim", parent: john)
irb(main):003:0> bob = Person.create(name: "Bob", parent: john)
irb(main):004:0> john.children.map(&:name)
=> ["Jim", "Bob"]
For an advanced challenge: Update the Person
model so that you can also get a list of all
of a person's grandchildren, as illustrated below. Would you need to make any changes to
the corresponding table in the database?
irb(main):001:0> sally = Person.create(name: "Sally")
irb(main):002:0> sue = Person.create(name: "Sue", parent: sally)
irb(main):003:0> kate = Person.create(name: "Kate", parent: sally)
irb(main):004:0> lisa = Person.create(name: "Lisa", parent: sue)
irb(main):005:0> robin = Person.create(name: "Robin", parent: kate)
irb(main):006:0> donna = Person.create(name: "Donna", parent: kate)
irb(main):007:0> sally.grandchildren.map(&:name)
=> ["Lisa", "Robin", "Donna"]
Normally, the target class of an ActiveRecord
association is inferred from the
association's name (a perfect example of "convention over configuration"). It is possible
to override this default behavior, though, and specify a different target class. Doing so,
it is even possible to have relationships between two objects of the same class.
This is how it is possible to set up a parent-child relationship. The model definition would look like:
class Person < ActiveRecord::Base
belongs_to :parent, class: Person
has_many :children, class: Person, foreign_key: :parent_id
end
It is necessary to specify the foreign_key
option for the has_many
relationship
because ActiveRecord
will attempt to use :person_id
by default. In the migration to
create the table for this model you would need to define, at minimum, a column for the
name
attribute as well as an integer column for parent_id
.
Self-referential relationships can be extended in all the same ways as normal two-model
relationships. This even includes has_many ... :through => ...
style relationships.
However, because we are circumventing Rails' conventions, we will need to specify the
source of the :through
in the case of adding a grandchild
relationship:
class Person < ActiveRecord::Base
belongs_to :parent, class: Person
has_many :children, class: Person, foreign_key: :parent_id
has_many :grandchildren, class: Person, through: :children, source: :children
end
Consequently, since this is still just using the parent_id
defined in the first case, no
changes to the table in the database are required.
What paths (HTTP verb and URL) will be defined by the following line in
config/routes.rb
?
resources :posts do
member do
get 'comments'
end
collection do
post 'bulk_upload'
end
end
Using the resource
method to define routes will automatically generate routes for the
standard seven restful actions:
GET /posts
POST /posts
GET /posts/new
GET /posts/:id/edit
GET /posts/:id
PATCH/PUT /posts/:id
DELETE /posts/:id
Note that Rails also supports the (relatively) new URL verb PATCH
for partial updates to
records. (In theory, a PUT
request should only be valid if the entire record is included
in the request).
The extra routes defined inside of the block passed to resources
will generate one route
valid for individual posts:
GET /posts/:id/comments
and one defined for the top-level resource:
POST /posts/bulk_upload