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
endBecause 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]
endThe 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
endThis 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]).allThis 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
endIt 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
endConsequently, 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
endUsing the resource method to define routes will automatically generate routes for the
standard seven restful actions:
GET /postsPOST /postsGET /posts/newGET /posts/:id/editGET /posts/:idPATCH/PUT /posts/:idDELETE /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