Rails Concepts in Short
Rails Concepts in Short
_______ ___
|
_ | | _ || |
| | || | |_| || |
| |_||_ |
|| |
|
__ ||
|| |
| | | || _ || |
|___| |_||__| |__||___|
___
_______
| |
|
|
| |
| _____|
| |
| |_____
| |___ |_____ |
|
| _____| |
|_______||_______|
Rails routing :
"rake routes" can be used to see all routes.
Basic routing :
get 'products/:id' , to: 'products#show'
or
get 'products/:id' => 'products#show'
Multiple HTTP Verbs on a single route :
match 'products/:id' => 'products#show' , via: [ :get, :post]
match 'products' => 'products#index' , via: :any <- This will match any verb
Generating URL:
link_to "Products" , controller: " products" , action: " show" , id: 1
Inserting hard coded parameters like :controller, :action
get 'products/special' => 'products#show' , special : 'true'
Optional route parameters can be mentioned using bracket :
match ':controller(/:action(/:id(.:format)))' , via: :any
Redirecting using routes
get " /foo" , to: redirect('/bar')
get " /google" , to: redirect('https://google.com/')
Redirect with status and propagating parameters
match " /api/v1/:api ", to: redirect( status: 302) { | params| " /api/v2/#{ para
ms[ :api ].pluralize} " }, via: :any
Constraints :
get ':controller/show/:id' => :show, constraints: { :id => /\d+/}
get ':controller/show/:id' => :show_error
The regular expression is similar to ^\d+$, rails implicitly adds the rest. If t
he first route fails then the second one will show errors.
First route in short : get ':controller/show/:id' => :show, id: /\d+/
get ':controller/:action/:id' => :show, constraints: { subdomain: 'admin' } (?)
Custom constraint:
get 'records/:id' => " records#protected" , constraints: proc { | req| req.param
s[ :id].to_i < 100 }
Route Globbing :
-> /items/list/base/books/fiction/dickens
get 'items/list/*specs' , controller: 'items' , action: 'list'
def list
specs = params[ :specs] # e.g, "base/books/fiction/dickens"
end
/aucti
Named scope:
scope :auctions, as: 'admin' do
get 'new' => :new, as: 'new_auction'
end
This will generate admin_new_auction_url
Namespace : Name Prefix (Named scope) + Path Prefix
namespace :auctions, :controller => :auctions do
get 'new' => :new
get 'edit/:id' => :edit
post 'pause/:id' => :pause
end
So this will be responding to /auctions/new and helpers will be created like auc
tions_new_url (?)
Constraint can be specified at scope level,
scope controller: :auctions, constraints: { :id => /\d+/} do
get 'edit/:id' => :edit
post 'pause/:id' => :pause
end
But routing will break if one of the route does not use the :id segment variable
, so,
scope path: '/auctions' , controller: :auctions do
get 'new' => :new
constraints id: /\d+/ do
get 'edit/:id' => :edit
post 'pause/:id' => :pause
end
end
To enable modular reuse, you may supply the constraints method with an object th
at has a matches? method.
class DateFormatConstraint
def self. matches?(request)
request. params[ :date] =~ /\A\d{4}- \d\d- \d\d\z/ # YYYY-MM-DD
end
end
in routes.rb
constraints( DateFormatConstraint) do
get 'since/:date' => :since
end
Resources :
Restful Resources in Rails can be declared like
resources :auctions
This defines four routes
GET
auctions_path
ndex
POST
auctions_path
reate
GET
auction_path(auctionObject)
how
PATCH auction_path(auctionObject)
pdate
DELETE auction_path(auctionObject)
estroy
-> /auctions/
-> AuctionsController::i
-> /auctions/
-> AuctionsController::c
-> /auctions/1
-> AuctionsController::s
-> /auctions/1
-> AuctionsController::u
-> /auctions/1
-> AuctionsController::d
$ rake routes
profile
POST
new_profile GET
edit_profile GET
GET
PATCH
PUT
DELETE
/profile(.:format)
/profile/new(.:format)
/profile/edit(.:format)
/profile(.:format)
/profile(.:format)
/profile(.:format)
/profile(.:format)
profiles#create
profiles#new
profiles#edit
profiles#show
profiles#update
profiles#update
profiles#destroy
Nested resources
resources :auctions do
resources :bids
end
This will create new_auction_url, edit_auction_url, auction_url and auctions_ur
l
as well as new_auction_bid_url, edit_auction_bid_url, auction_bid_url and aucti
on_bids_url
Using with link_to ->
link_to " See all bids" , auction_bids_path(auction)
link_to " Delete this bid" , auction_bid_path(auction, bid), method: :delete
# This in short can be written like
link_to " Delete this bid" , [ auction, bid] , method: :delete # Rails will fi
gure out that it is nested route
Shallow routes :
This will shorten the url for nested component by removing parent resource ref
erence when parent component is not required,
resources :auctions, shallow: true do
resources :bids
end
or
resources :auctions, shallow: true do
shallow do
resources :bids
end
end
Result of above : -> (_path or _url should be appended with the names specifie
d in left most side)
auction_bids
GET
/auctions/:auction_id/bids(.:format)
POST
/auctions/:auction_id/bids(.:format)
new_auction_bid
GET
/auctions/:auction_id/bids/new(.:format)
edit_bid
GET
/bids/:id/edit(.:format) [Removed auction as edi
ting a single bid should not require auction, it is having its own unique id]
bid
GET
/bids/:id(.:format) [For showing a specific bid
auction is not required]
PATCH /bids/:id(.:format)
PUT
/bids/:id(.:format)
DELETE /bids/:id(.:format)
auctions
GET
/auctions(.:format)
POST
/auctions(.:format)
new_auction
GET
/auctions/new(.:format)
edit_auction
GET
/auctions/:id/edit(.:format)
auction
GET
/auctions/:id(.:format)
PATCH /auctions/:id(.:format)
PUT
/auctions/:id(.:format)
DELETE /auctions/:id(.:format)
Routing concerns :
resources :auctions do
resources :bids
resources :comments
resources :image_attachments, only: :index
end
resources :bids do
resources :comments
end
In the above case comment is required in both auction and bid, thus a concern
can be created with that
concern :commentable do
resources :comments
end
concern :image_attachable do
resources :image_attachments, only: :index
end
Then can be used like :
resources :auctions, concerns: [ :commentable, :image_attachable] do
resources :bids
end
resources :bids, concerns: :commentable
Additional Routes with REST :
If with bid we want to implement retraction of bid, then we will be needing tw
o routes a "GET" which will give the form and a "POST" which will actually save
the retraction status.
resources :auctions do
resources :bids do
member do
get :retract
post :retract
end
end
end
This will be generating something like
link_to " Retract" , retract_bid_path(auction, bid)
link_to " Retract" , retract_bid_path(auction,bid), method: :post
The upper route can be written in short as
resources :auctions do
resources :bids do
member do
match :retract, via: [ :get, :post]
end
end
end
or even shorter way..
resources :auctions do
resources :bids do
match :retract, via: [ :get, :post] , on: :member
end
end
Additional routes on complete collection :
resources :auctions do
collection do
match :terminate, via: [ :get, :post]
end
end
Previous section was on member where the additional route will be created like b
ids/1/retract but extra routes on: :collection will be created like /bids/termin
ate.
Can also be written as
resources :auctions do
match :terminate, via: [ :get, :post] , on: :collection
end
Changing usual action name for REST routes:
resources :projects, path_names: { new: 'nuevo' , edit: 'cambiar' }
To a different controller : resources :photos, controller: " images"
The data delivered delivered or sent to server in REST is not resource, it is a
representation of resource. The exchange currency in REST is representation.
A controller can respond to different representation request (html, xml, json) b
y using respond_to method
respond_to do | format|
format. html
format. xml { render xml : @auctions }
end
Or in short using respond_with
respond_with( @auctions)
respond_with will do the following in case of a json request like bids/1.json
Attempt to render the associated view with a .json extension.
If no view exists, call to_json on the object passed to responds_with.
If the object does not respond to to_json, call to_format on it.
Controllers:
Rack:
Rack is responsible for processing request and producing response, within this
process Rails is invoked as a part.
class HelloWorld
def call (env)
[200, {"Content-Type" => "text/plain" }, [ "Hello world!" ]]
end
end
Rails introduces different middle wares through which the request is passed be
fore it ends up in some controller action. Each of these middle ware performs so
me action or collect some data, example checking if any migrations are pending,
initiating ActiveRecord connection management, initiating logger etc. We can int
roduce the middle ware from config/application.rb
module Example
class Application < Rails:: Application
. . .
301
302
303
307
->
->
->
->
Permanent
Temporary
Temporary
Temporary
redirect
redirect
redirect with GET request irrespective of actual verb
redirect (with the requesting verb), eg: If an account ex
pired
def paid_resource
if current_user.account_expired?
response.headers['Location'] = account_url(current_user)
render text: "Account expired", status: 307
end
end
401 -> Unauthorized (can be used with API)
403 -> Forbidden (some service may be hiding behind login and if accessed
with out a logged in user then server denies to serve the request)
Can be used with google bots
def index
return render nothing: true, status: 403 unless logged_in?
@favorites = current_user.favorites.all
end
404 -> Not found
410 -> Gone : Previously existed but currently gone
500 -> Error in server internal code
503 -> Site taken down for maintenance
Specifying layouts :
layout " events" , only: [ :index, :new]
layout " global " , except: [ :index, :new]
Redirecting :
As HTTP 1.0 301, 302 was used for permanent and temporary redirect respectively
. 301 can be used to redirect after a resource creation (a POST request for crea
tion and a GET request after that for index) but when using 302 it could mean th
at the original POST request is being processed by a different action that is do
ing the same POST request to another action or a GET request to index. To remove
this ambiguity 303 and 307 was introduced, where 303 will always mean a GET req
uest during redirect what ever the source request verb was, thus can be used for
redirecting after resource creation. The 307 will always use the same method as
the original one, can be used like
response.headers['Location'] = account_url(current_user)
render text: "Account expired", status: 307
** url_for can be used to generate URL
redirect_to(target, response_status = {})
redirect_to
redirect_to
redirect_to
redirect_to
redirect_to
redirect_to
after, before or around class method need to be declared, and they also rece
ives a controller object to play with.
Inline callback:
before_action do
redirect_to new_user_session_path unless authenticated?
end
prepend_before_action, prepend_after_action can be used to push callback at th
e beginning of before or after callback chain respectively.
A nice use of around exception
around_action :catch_exceptions
private
def catch_exceptions
# do stuff before action
yield
# do stuff after action
rescue => exception
logger. debug " Caught exception! #{ exception} "
raise
end
Inline around_action :
around_action do | controller, action|
logger.debug " before #{ controller. action_name} "
action.call
logger. debug " after #{ controller. action_name} "
end
Benchmarking actions:
around_action BenchmarkingActionCallback
class BenchmarkingActionCallback
def self.around(controller)
Benchmark.measure { yield }
end
end
Skipping a callback from child class:
skip_before_action :authenticate
skip_action_callback :catch_exceptions # for around exeception or general (?)
Limiting action callback:
before_action :authorize, only: [ :edit, :delete]
around_action except: :index do | controller, action_block|
results = Profiler.run( &action_block)
controller.response.sub! " </body> " , " #{ results} </body> "
end
Live streaming using Rails : (There is a detailed article at tenderlove -> Bookm
arks)
class StreamingController < ApplicationController
include ActionController::Live # This is the main mixin
# Streams about 180 MB of generated data to the browser.
def stream
10_000_000.times do | i |
g mechanism).
public_dir = File.join( Rails.root, 'public', controller_path) #/public/auctio
ns/show/
FileUtils.mkdir_p(public_dir)
FileUtils.cp(filename, File.join(public_dir, filename)) #/public/auctions/show
/1.json (?) or may be my_generate_tarball_method( 'dir' ) this stuff
send_file '/path/to.jpg', type: 'image/jpeg', disposition: 'inline'
# Streaming a video :)
send_file @video_file.path, filename: video_file.title + '.flv', type: 'video/
x-flv', disposition: 'inline'
Using Action Pack Variant :
class ApplicationController < ActionController:: Base
before_action :set_variant
def index
respond_to do | format|
format.html do | html |
html.mobile do # renders app/views/posts/index.html+mobile.haml
@mobile_only_variable = true
end
end
end
end
protected
def set_variant
request.variant = :mobile if request.user_agent =~ /iPhone/i
end
end
Active Record:
--------------Some configurations:
self.table_name = "xxx" # Can be used to set table for that model
self.primary_key = "mno" # Can be used to set the primary key other than 'id
'
In config/application.rb -->
# To stop pluralizing the name of model to get the table name
config.active_record.pluralize_table_names = false
config.active_record.primary_key_prefix_type
If the value is
:table_name -> Active Record will look for <table>id instead of id as t
he primary column
:table_name_with_underscore -> Active Record will look for <table>_id i
nstead of id as the primary column
config.active_record.table_name_prefix
# Prefixes each table name with some string
config.active_record.table_name_suffix
Reading and writing attribute values on a model:
In model, the attributes are not declared explicitly. Rails will read databa
se schema during read or write to dynamically create or use them.
read_attribute(:category) can be used for reading an attribute value and wri
the first one will return true and second one will return false in such cases.
c = Client. new do |client|
client.name = " Nile River Co. "
client.code = " NRC"
end
or
c = Client. create( name: " Nile River, Co. " , code: " NRC" )
Reading:
first_project = Project.find(1)
# Works as select *
all_clients = Client.all
first_client = Client.first
last_client = Client.last
Product.find([1, 2]) # tries to match all records with specified primary key
s
Reading and writing members:
Object fashion : first_client.name
Hash fashion : first_client[:name] or first_client['name']
read_attribute and write_attribute can be used to read or write properties
as well.
-> first_client.attributes
attributes method can be used to return a hash of all attributes for that mo
del, it is a shallow copy and won't contain the associations.
-> Attribute values are type casted as per the database field, the value bef
ore type casting can be retrieved using the accessor <fieldname>_before_type_cas
t
example : rate_before_type_cast.tr( '$,', '' )
Reload function can be used to fire the SQL queries again and reload the data
from database for a specific object.
record.reload(lock: true) # reload the same record with an exclusive row lock
Clone : This can be used to shallow copy an active record row object.
Firing raw SQL Queries : Client.find_by_sql( "select * from clients" )
Making the query parameterized : Client. where( "code like ?" , "#{ param} %"
) # This will properly quote the value and provide protection from SQL Injection
and other stuff
Client.count_by_sql( "select count(*) from clients" )
Query Cache:
Rails query cache is simple it saves the result as the value where the query
is the key. It will treat same queries but written differently as two different
queries (limitation).
The cache can be manually invoked as well,
User.cache do
puts User.first
puts User.first
puts User.first
end
Second and third request will be processed from the cache.
clear_query_cache can be used to clear cache manually.
Update :
A record can be updated by calling update method on model where first argume
nt is and id or a list of ids, the second argument expects a single hash or a se
t of hashesh.
Can be directly used with "if",
if project.update(params[ :project])
redirect_to project
else
render 'edit'
end
or valid method can be called on object to check if the validations passed o
r not but this will rerun the validations as well.
Mass updating using update_all :
Project.update_all({ manager: 'Ron Campbell' }, technology: 'Rails' )
Project.update_all( "cost = cost * 3" , "lower(technology) LIKE '%microsof
t%'" )
Here the first parameter is treated as update intent and second parameter as
query string.
Another way to update:
proj = Project.find(2)
proj.manager = "XYZ"
proj.save # or we can use proj.save!
proj.save will return true or false according to validation passed or not
where banged save will raise exception
update_attribute is also a way to update an attribute which receives either
a key value pair as arguments (proj.update_attribute(:manager, 'XYZ')) or a hash
of key value pairs but this will skip all validations.
[this method allows you to persist an Active Record model to the database ev
en if the full object isn't valid]
update_column is almost similar to update_attribute but along with validatio
ns it also skips updating the updated_at time stamp and do not run any callbacks
.
update_columns (plural of update_column) accepts a hash of key value pair an
d acts similar to its singular version
Another way of mass assignment is by association, so if Project contains has
_many :users then Rails will expose user_ids attribute writer, so in form if any
checkbox selection of user is available we can just use the name proj[user_ids]
[] and rails will take care of rest (but can expose to security risk (?))
increment, decrement, and toggle can also be used with fields (?) , each has
a bang variant which will persist the value in db by calling update_attributes
Touching the records:
>> user = User.first
>> user.touch # => sets updated_at to now.
Trying to write a read-only value will not raise any exception only that v
alue won't be written again if already written once.
To get a list of read-only attributes : >> Customer.readonly_attributes
Destroying a record:
>> bad_timesheet = Timesheet.find(1)
>> bad_timesheet.destroy
>> bad_timesheet.user_id = 2 # Trying to modify again, will not work
RuntimeError: can't modify frozen Hash
destroy! will raise exception if record can not be destroyed
delete (along with a version of destroy) method exists at class level which
accepts an id or an array of ids which will be deleted with out loading their in
stance in the memory. So this method is fast but it will get into trouble if any
after delete callback or association deletion requirement is present.
Locking
1. Optimistic Locking : Will find if the collision happened and try to resolve
Adding column named lock_version in the table with a default value of zero
is sufficient.
Behavior:
describe Timesheet do
it "locks optimistically" do
t1 = Timesheet.create
t2 = Timesheet.find(t1.id)
t1.rate = 250
t2.rate = 175
expect(t1.save).to be_true
expect { t2.save }.to raise_error(ActiveRecord:: StaleObjectError)
end
end
Migration when adding lock_version
def change
add_column :timesheets, :lock_version, :integer, default: 0
end
Locking column (lock_version) can be changed at application level by using
config.active_record.locking_column = :alternate_l_c # at application.rb
At model level using
self.locking_column = :alt_l_c
Handling code at controller could be
def update
timesheet = Timesheet.find(params[:id])
timesheet.update(params[:timesheet] )
# redirect somewhere
rescue ActiveRecord::StaleObjectError
flash[:error] = " Timesheet was modified while you were editing it."
redirect_to [:edit, timesheet]
end
2. Pessimistic Locking : Will not load an instance of row if it is locked by a
ny other
Requires database support, can be used in transaction
Timesheet.transaction do
t = Timesheet.lock.first
t.approved = true
t.save!
end
It's also possible to call lock! on an existing model instance, which simp
ly calls reload(lock: true) under the covers. You wouldn't want to do that on an
instance with attribute changes since it would cause them to be discarded by th
e reload. If you decide you don't want the lock anymore, you can pass false to t
he lock! method.
Pessimistic locking is a bit easier to implement, but can lead to situatio
ns where one Rails process is waiting on another to release a database lock, tha
t is, waiting and not serving any other incoming requests. **Remember that Rails
processes are typically single-threaded.**
Where in Active Record:
Product.where( sku: params[:sku] )
Product.where( sku: [ 9400, 9500, 9900] ) # Uses IN when passed an array
Product.where('description like ? and color = ?', "%#{terms}%", color)
Product.where('sku in (?)', selected_skus)
Article.where.not( title: 'Rails 3' )
Article.where.not( title: ['Rails 3', 'Rails 5'] ) # Uses NOT IN internally
Product.where( "name = :name AND sku = :sku AND created_at > :date" , name: "S
pace Toilet" , sku: 80800, date: '2009-01-01' )
Message.where( "subject LIKE :foo OR body LIKE :foo" , foo: '%woah%' ) # Nice!
User.where( login: login, password: password).first # Uses AND
>>
=>
>>
=>
>>
=>
extending
module Pagination
def page(number)
# pagination code
end
end
scope = Model.all.extending(Pagination)
scope.page(params[:page])
Group BY and Having
>> users = Account.select('name, SUM(cash) as money').group('name').to_a
>> User.group("created_at").having(["created_at > ?", 2.days.ago])
includes, eager_load and preload :
>> users = User.where(login: "mack").includes(:billable_weeks)
To remove n+1 query issues we can load some association with the main model us
ing includes, we can even load second or third degree of association
>> clients = Client.includes( users: [:avatar] ) # This loads association avat
ar with in users (?)
>> Client.includes(
users: [:avatar, { timesheets: :billable_weeks }]
)
eager_load and preload can be used interchangeably (?)
Join :
Buyer.select('buyers.id, count(carts.id) as cart_count')
.joins('left join carts on carts.buyer_id = buyers.id')
.group('buyers.id')
Use of none :
def visible
case role
when :reviewer
Post.published
when :bad_user
Post.none
end
end
# If chained, the following code will not break for users
# with a :bad_user role
posts = current_user.visible.where( name: params[:name] )
So for bad user role it will post an object which will not query anthing in da
tabase but also it will not return nil so developer don't have to check before a
ppending other chains, it won't simply query database
Use of readonly :
If anything retrieved with readonly key word any changes on it will be discard
ed.
>>
=>
>>
=>
>>
=>
c = Comment.readonly.first
#<Comment id: 1, body: "Hey beeyotch!">
c.body = " Keep it clean! "
"Keep it clean!"
c.save
ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
references :
When a column name is used in the query (a string query) from an association t
hen along with "includes" (which will eager load the association), "references"
method need to be used.
Team.includes(:members).where("members.name = ?", "Tyrion") -> This will raise
an exception
but
Team.includes(:members).where("members.name = ?", "Tyrion").references(:member
s) -> will work fine
but
Team.includes(:members).where(members: {name: 'Tyrion'}) -> will work fine
and
Team.includes(:members).order("members.name") -> will work fine
reorder :
This will discard effect of any previous order method calls (?)
Member.order("name DESC").reorder(:id) # SELECT * FROM members ORDER BY member
s.id ASC
But any other order after reorder will be appended with the reordered value
Member.order("name DESC").reorder(:id).order(:name) # SELECT * FROM members OR
DER BY members.name ASC, members.id ASC
#### How ordering changed!! (?) It should come like ORDER BY id ASC, name ASC
Member.order(:name).reverse_order # SELECT * FROM members ORDER BY members.nam
e DESC
uniq/distinct :
User.select(:login).uniq # SELECT DISTINCT login FROM "users"
Unscoping an constraint (removing):
Member.order('name DESC').unscope(:order) # SELECT "members".* FROM "members"
Member.where(name: "Tyrion" , active: true).unscope(where: :name) === Member.w
here( active: true)
Using Arel :
https://github.com/rails/arel
>> users = User. arel_table
>> users. where(users[ :login] . eq( " mack" )) . to_sql
Connecting to multiple databases:
The best method is to develop a base class which will inherit ActiveRecord::Ba
se and call establish_connection (part of ActiveRecord::Base) inside it, and cre
ate models which will use that connection should be created by inheriting that b
ase class.
class SomeOtherDB < ActiveRecord::Base
establish_connection :someother_db # someother_db is written in database.yml
self.abstract_class = true
end
establish_connection recieves an key from database yml as a symbol or the conn
ection details hash directly (poor way of doing stuff)
Using connection object from ActiveRecord:
ActiveRecord::Base.connection.execute("SELECT * FROM blah").values
ActiveRecord::ConnectionAdapters::DatabaseStatements is responsible for some u
seful functions :
conn = ActiveRecord::Base.connection
conn.begin_db_transaction()
conn.commit_db_transaction()
conn.delete(sql_statement) # Executes a SQL DELETE statement provided and re
turns the number of rows affected.
conn.execute(sql_statement) # This method is abstract in the DatabaseStateme
nts module and is overridden by specific database adapter implementations.
conn.insert(sql_statement) # Executes an SQL INSERT statement and returns th
e last autogenerated ID from the affected table.
conn.reset_sequence!(table, column, sequence = nil) # Used in Oracle and Pos
tgres; updates the named sequence to the maximum value of the specified table's
column.
conn.rollback_db_transaction()
conn.select_all(sql_statement)
conn.select_one(sql_statement) # Selects the first tupple
conn.select_value(sql_statement) # Selects the first column of first tupple
conn.select_values(sql_statement) # Selects the first column of all tupples
conn.update(sql_statement) # Executes the update statement provided and retu
rns the number of rows affected. Works exactly like delete.
conn.active? # Checks if the connection is alive
conn.adapter_name
conn.disconnect!
conn.reconnect!
conn.raw_connection
conn.supports_count_distinct? # Indicates whether the adapter supports using
DISTINCT within COUNT in SQL statements. This is true for all adapters except S
QLite
conn.supports_migrations?
conn.tables # Produces a list of tables in the underlying database schema. I
t includes tables that aren t usually exposed as Active Record models, such as schem
a_info and sessions.
conn.verify!(timeout) # Lazily verify this connection, calling active? only
if it hasn t been called for timeout seconds.
Configs
ActiveRecord::Base.default_timezone # Time.local (using :local ) or Time.u
tc (using :utc), Defaults to :local
ActiveRecord::Base.schema_format # :sql, :ruby
ActiveRecord::Base.store_full_sti_class
Migrations:
Migration creates schema_migrations table in the database which will keep the
version of the migration.
rake db:migrate will check this table and run all the migrations created after
the version
change :
class CreateClients < ActiveRecorc::Migration
def change
create_table :clients do |t|
t.string :name
t.string :code
t.timestamps # adds the created_at and updated_at time stamp fields
end
end
end
To roll back to a specific version
rake db:migrate VERSION=20130313005347
reversible :
To help migration reversing,
class EnablingHStore < ActiveRecord::Migration
def change
reversible do |dir|
dir.up do
execute("CREATE EXTENSION hstore")
end
dir.down do
execute("DROP EXTENSION hstore")
end
end
add_column :users, :preferences, :hstore
end
end
To make a migration irreversible, change down to raise exception
dir.down { raise ActiveRecord::IrreversibleMigration }
Create Table:
Stopping migration's create_table from adding auto increment id
def change
create_table :clients, id: false do |t|
...
end
end
If we send a symbol instead of false in id: then the auto increment key will b
e created with that name instead of conventional 'id'
Other options with create_table
:force -> It will drop the table if exists
:options -> Developer can provide database specific statements
:temporary -> if set to true table will exists for current session (during m
igration) - needed to migrate huge dataset (?)
Change Table : change_table similar to create table (need more info) (?)
Create join tables :
create_join_table :ingredients, :recipes
Accepts two table names and creates a table named ingredients_recipes with n
o primary key
The methods of t (available with in create_table and change_table :P)
t.change(:name, :string, limit: 80)
t.change(:description, :text)
t.change_default(:authorized, 1)
t.column(:name, :string)
Or in short
t.string(:goat)
t.string(:goat, :sheep)
t.index(:name) # Creates an index in the database
t.index([:branch_id, :party_id], unique: true)
t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party') # Na
med index
t.belongs_to(:person)
t.belongs_to(:person, index: true) # adds an index on foreign key
t.belongs_to(:person, polymorphic: true) # adds a _type column as well
## belongs_to and references are same method
t.remove(:qualification)
t.remove(:qualification, :experience)
t.remove_index column: :branch_id
t.remove_index column: [:branch_id, :party_id] # remove the accounts_branch_
id_party_id_index from the accounts table
t.remove_index name: :by_branch_party
t.remove_belongs_to(:person)
t.remove_references(:commentable, polymorphic: true)
t.remove_timestamps
t.rename(:oldname, :newname)
t.timestamps #adds created_at, updated_at
Another way to add column
add_column :clients, :code, :string
add_column :clients, :created_at, :datetime
# This should not be used inside create_table/change_table, should be just
used inside change method
Floating-point numbers are by nature imprecise, so it is important to choose
:decimal instead of :float for most business-related applications.
Using Migration for Data Migration :
Converting Phone number components to single entity
class CombinePhoneNumber < ActiveRecord::Migration
def change
add_column :phones, :number, :string
reversible do |dir|
dir.up do
execute( "update phones set number = concat(area_code, prefix, suf
fix)" )
end
dir.down { ... }
end
remove_column :phones, :area_code
remove_column :phones, :prefix
remove_column :phones, :suffix
end
end
We can do the same thing using Phone model but that is slower
Phone.find_each do |phn|
phn.number = phn.area_cdoe + phn.suffix + phn.prefix
phn.save
end
And also we can do
Phone.update_all("number = concat(area_code, prefix, suffix)")
Models can be declared with in Migrations!
class HashPasswordsOnUsers < ActiveRecord:: Migration
class User < ActiveRecord:: Base
end
def change
reversible do |dir|
dir.up do
add_column :users, :hashed_password, :string
User.reset_column_information
User.find_each do |user|
user.hashed_password = Digest:: SHA1 .hexdigest(user.password)
user.save!
end
remove_column :users, :password
end
dir.down { raise ActiveRecord:: IrreversibleMigration }
end
end
end
db/schema.rb contains the updated and final database state and can be used to re
create the database (Running all migrations is not a good choice)
rake db:schema:load to create your database schema from scratch without having
to run all migrations.
db/seed.rb This can be used as a default location for creating seed data for you
r database.
rake db:seed
For dummy data that will be only used on development or staging, I prefer to c
reate custom rake
tasks under the lib/tasks directory, for example lib/tasks/load_dev_data.rake.
This helps keep
seed.rb clean and free from unnecessary conditionals, like unless Rails.env.pr
oduction?
Rake tasks for migration :
rake db:create # Create the database defined in config/database.yml for the cu
rrent Rails.env
rake db:create:all #create all of the local databases defined in config/databa
se.yml
rake db:drop # Drops the database for the current RAILS_ENV
rake db:drop:all # drops all of the local databases defined in config/database
.yml
rake db:forward # forward the db version by one version after rollback
rake db:rollback # rollback to previous migration version
roxy (?)
On association we can use natural active record operations like where, order e
tc
amy = User.find(1)
amy.timesheets.any? (if more then zero records exists for the association) amy
.timesheets.any? -> returns true if at least one time sheet exists
amy.timesheets.many? -> returns true if more than one record exists
amy.timesheets.empty? -> returns true if zero record exists
amy.expense_reports.average(:expense_value).to_f # Gives average of expense_va
lue field of expense_report
We can also create using
user.timesheets.new(attributes)
2.3.0 :022 > amy.expense_reports.new # -------> Trying to create new
=> #<ExpenseReport id: nil, desc: nil, user_id: 1, created_at: nil, updated
_at: nil, expense_value: nil>
2.3.0 :023 > amy.expense_reports # -------------> the new one is appended at
the end
=> #<ActiveRecord::Associations::CollectionProxy [#<ExpenseReport id: 1, de
sc: nil, user_id: 1, created_at: "2016-08-20 13:00:51", updated_at: "2016-08-20
13:05:51", expense_value: #<BigDecimal:21eac38,'0.525E1',18(36)>>, #<ExpenseRepo
rt id: 2, desc: "Another report", user_id: 1, created_at: "2016-08-20 13:08:20",
updated_at: "2016-08-20 13:08:20", expense_value: #<BigDecimal:21e67f0,'0.5E1',
9(27)>>, #<ExpenseReport id: nil, desc: nil, user_id: 1, created_at: nil, update
d_at: nil, expense_value: nil>]>
2.3.0 :024 > amy.expense_reports.last # -------------> Referring the new one
using last
=> #<ExpenseReport id: nil, desc: nil, user_id: 1, created_at: nil, updated
_at: nil, expense_value: nil>
2.3.0 :025 > amy.expense_reports.last.desc = "Well another one" -------> Set
ting stuff
=> "Well another one"
2.3.0 :026 > amy.expense_reports.last.expense_value = 2
=> 2
2.3.0 :027 > amy.expense_reports.last.save --> Saving
(0.2ms) begin transaction
SQL (0.5ms) INSERT INTO "expense_reports" ("desc", "user_id", "created_at
", "updated_at", "expense_value") VALUES (?, ?, ?, ?, ?) [["desc", "Well anothe
r one"], ["user_id", 1], ["created_at", 2016-08-20 13:14:27 UTC], ["updated_at",
2016-08-20 13:14:27 UTC], ["expense_value", #<BigDecimal:4834d08,'0.2E1',9(27)>
]]
(15.3ms) commit transaction
=> true
2.3.0 :028 > amy.expense_reports.count
(0.4ms) SELECT COUNT(*) FROM "expense_reports" WHERE "expense_reports"."
user_id" = ? [["user_id", 1]]
=> 3
amy.expense_reports.calculate(:sum, :expense_value).to_f
(0.3ms) SELECT SUM("expense_reports"."expense_value") FROM "expense_report
s" WHERE "expense_reports"."user_id" = ? [["user_id", 1]]
=> 12.25
# Calculate can be used to aggregate data, first argument is the operation na
me which are :minimum, :maximum, :average, :sum and the second one is the column
name
amy.expense_reports.count(:column_name)
Select
user.timesheets.select(:submitted).to_a
user. timesheets. select( [ :id, :submitted] ) . to_a
sum(column_name, options = {}) # Convenience wrapper for calculate(:sum, ...)
uniq # equality of Active Record objects is determined by identity, meaning th
at the value of the id attribute is the same for both objects being compared
belongs_to association :
Timesheet.user will return the user associated with it but if need to be rel
oaded we can use Timesheet.user(true)
To create owner object from this side we can use build_<owner> or create_<ow
ner>; build_<owner> (eg. build_user) won't save it in the database but create_ow
ner will.
Association configurations for belongs_to:
autosave: true # automatically save the owning record whenever this record
is saved. Defaults to false.
class_name: # When a model class name is required as association name is n
ot same as model name
# In this case approvers are users only so we can add approver_id in times
heets table and configure the model like
class Timesheet < ActiveRecord:: Base
belongs_to :approver, class_name: 'User'
belongs_to :user
...
end
:counter_cache # Use this option to make Rails automatically update a counte
r field on the associated object with the number of belonging objects
counter_cache: true # will find field timesheets_count in users table and
save the count of timesheets ; default value of this field should be zero
Timesheet.reset_counters(5, :weeks) # first argument is id and second one is
column name
dependent: :destroy or :delete
# Usage of this option might make sense in a has_one / belongs_to pairing. H
owever, it is really unlikely that you want this behavior on has_many / belongs_
to relationship; it just doesn't seem to make sense to code things that way. Add
itionally, if the owner record has its :dependent option set on the correspondin
g has_many association, then destroying one associated record will have the ripp
le effect of destroying all of its siblings.
foreign_key: column_name
belongs_to :administrator, foreign_key: 'admin_user_id'
inverse_of: name_of_has_association (?)
polymorphic: true # By making a belongs_to relationship polymorphic, you abs
tract out the association so that any other model in the system can fill it.
create_table :comments do | t|
t.text :body
t.references :subject, polymorphic: true
end
class Account < ActiveRecord::Base
validates_with EmailValidator, attr: :email
end
:allow_blank and :allow_nil
# In some cases, you only want to trigger a validation if a value is present
# :allow_blank option skips validation if the value is blank
# :allow_nil option skips the validation if the value of the attribute is nil
:message
# Message can be provided using this, available variable for interpolation
1. count
2. value -> current value of the attribute
3. model
4. attribute (?)
:on # validation can be restricted on creation (:create) or on updation only (
:update)
validates_uniqueness_of :email , on: :create
validates :email , presence: { strict: true } (?)
validates_presence_of :approver, if: -> { approved? && ! legacy? }
validates_presence_of :password, if: :password_required?
with_options if: :password_required? do |user|
user.validates_presence_of :password
user.validates_presence_of :password_confirmation
user.validates_length_of :password, within: 4..40
user.validates_confirmation_of :password
end
Validation context can be used for conditional validation as well. We can
validates_presence_of :name, on: :publish
Here :on argument contains the context name, now this rule will be applied onl
y when
report.valid? :publish
is called, that means valid?(context_name) need to be called for this validati
on
Using multiple validators using validates
validates :username, presence: true,
format: { with: /[A-Za-z0-9]+/ },
length: { minimum: 3 },
uniqueness: true
validates :unwanted, absence: { message: " You shouldn't have set that" } #
Alias for validates_absence_of
validates :terms, acceptance: { message: 'You must accept terms.' }
validates :email , confirmation: { message: 'Try again.' }
validates :username, exclusion: %w( admin superuser)
validates :username, format: /[A-Za-z0-9]+/ # or format: /[A-Za-z0-9]+/ (?)
validates :gender, inclusion: %w( male female)
validates :username, length: 3..20 # length: { minimum: 0, maximum: 1000 }
validates :quantity, numericality: { message: 'Supply a number.' }
validates :username, presence: { message: 'How do you expect to login?' }
validates :username, uniqueness: { message: " You're SOL on that login choic
e, buddy! " }
Adding custom validation macros:
To create a custom validator that follows macro like style we need to inheri
t from ActiveModel::EachValidator
(There is also ActiveRecord::Validator)
class ReportLikeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value["Report"]
record.errors.add(attribute, 'does not appear to be a Report' )
end
end
end
validates :report, report_like: true
#### initialize method can be used to specify additional arguments
class ReportLikeValidator < ActiveModel::EachValidator
def initialize(options)
@with = options[:with]
super
end
def validate_each(record, attribute, value)
unless value[@with]
record.errors.add(attribute, 'does not appear to be like #{@with}')
end
end
end
validates :name, like: { with: " Report" }
Modifying errors hash:
errors[:base] = msg
# Adds an error message related to the overall object state itself and not t
he value of any particular attribute
errors[:attribute] = msg
# Adds an error message related to a particular attribute.
errors.clear
# As you might expect, the clear method clears the Errors collection
To access : # Always returns an array ***
user.errors.full_messages_for( :email )
user.errors[:login]
Advanced Active Record
Parameterized Scope
scope :newer_than, -> (date) { where( 'start_date > ?' , date) }
Invocation : BillableWeek.newer_than( Date.today)
scopes are available automatically on has_many association
scope :tardy, -> {
joins(:timesheets).
following Active Record methods, when executed, do not run any callbacks:
decrement
decrement_counter
delete
delete_all
increment
increment_counter
toggle
touch
update_column
update_columns
update_all
update_counters
# If you return a boolean false (not nil) from a callback method, Active Recor
d halts the execution chain. No further callbacks are executed. The save method
will return false, and save! will raise a RecordNotSaved error.
# The after_find callback is invoked whenever Active Record loads a model obje
ct from the database, and is actually called before after_initialize, if both ar
e implemented. Because after_find and after_initialize are called for each objec
t found and instantiated by finders.
# if you want to run some code only the first time that a model is ever instan
tiated, and not after each database load?
after_initialize do
if new_record?
. . .
end
end
Callback Class
class MarkDeleted
def self.before_destroy(model)
model.update_attribute( :deleted_at, Time. current)
false
end
end
before_destroy MarkDeleted # In any model
Single Table Inheritance (STI)
Single Table Inheritance is a method of depicting object oriented inheritanc
e between models in database. The database table will contain all data of all ch
ild classes with a type field.
For example
class Timesheet < ActiveRecord::Base
def billable_hours_outstanding
if submitted? # should check if the submitted column is true
billable_weeks.map(&:total_hours).sum # for each week get total hour
s by calling total_hours method on each billable week object, then get the sum
else
0
end
end
def self.billable_hours_outstanding(user) # Adding a class method in cas
e called like Timesheet.billable_hours_outstanding userObj (?)
user.timesheets.map(&:billable_hours_outstanding).sum
end
end
Well in this case if we only want to get non paid timesheets then we have to
modify billable_hours_outstanding instance method, which is violation of OCP
Will be converted like
def billable_hours_outstanding
if submitted? && not paid? # This is not good
billable_weeks.map(&:total_hours).sum
else
0
end
end
A good implementation would be
class Timesheet < ActiveRecord::Base
# non-relevant code ommitted
def self.billable_hours_outstanding_for(user)
user.timesheets.map(&:billable_hours_outstanding).sum
end
end
class DraftTimesheet < Timesheet
def billable_hours_outstanding
0 # Drafts are not submitted so by default zero, well it won't be bad
to predict expense by calculating drafter time sheet hours!?
end
end
class SubmittedTimesheet < Timesheet
def billable_hours_outstanding
billable_weeks.map(&:total_hours).sum
end
end
class PaidTimesheet < Timesheet
def billable_hours_outstanding
billable_weeks.map(&:total_hours).sum - paid_hours
end
end
In database if we add a column named type then ActiveRecord automatically be
haves to support STI (add_column :timesheets, :type, :string)
d = DraftTimesheet.create
d.type # DraftTimesheet
Inheritance column name can be overridden from type using
self.inheritance_column = 'object_type'
or
config.activerecord.inheritance_column (?)
Making an attribute enumerable
enum status: %i(draft published archived) # will go inside model
now one can do
post.draft! -> Set the status to draft
post.draft? -> output true
post.status -> output draft
Post.draft -> will return all posts which are drafts
# Active Record creates a class method with a pluralized name of the defined
enum on the model, that returns a hash with the key and value of each status.
>> Post.statuses
=> {"draft"=>0, "published"=>1, "archived"=>2}
Using state machine # https://github.com/pluginaweek/state_machine (Awesome)
Creating a commentable concern
module Commentable
= yield
# set a flash notice and not redirect, thereby incorrectly showing a flash me
ssage on the following request
flash.now.alert = " #{@post.title} could not be created. "
logger # same as Rails.logger
params # same params hash that is available in your controller, containing the
key/value pairs of your request
request, response and session are also available in view
Partials
= render 'details'
# Partial template names must begin with an underscore, which serves to set
them apart visually within a given view template directory
# However, you leave the underscore out when you refer to them.
= render 'details' , form: form
# Partial with variables passed to it
# To check presence of a certain local variable in a partial, you need to do
it by checking the local_assigns hash that is part of every template
- if local_assigns. has_key? :special
= special
= render entry # will render _entry.html.haml with "entry" object set a cont
ext (?)
Equivalent to
= render partial : 'entry' , object: entry
render entries # rendering collection will expect a template named _entry.ht
ml.haml and will pass an object named "entry"
equivalent to
partial : 'entry' , collection: @entries
# There is a counter for each partial, name of the variable is the name of t
he partial, plus _counter