Skip to content

Instantly share code, notes, and snippets.

@jennli
Last active March 17, 2020 21:32
Show Gist options
  • Save jennli/5a7ae71bd9daebbf0648 to your computer and use it in GitHub Desktop.
Save jennli/5a7ae71bd9daebbf0648 to your computer and use it in GitHub Desktop.
  • install postgres
brew install postgresql

ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents
~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist
  • install rails
gem install rails
  • T enables testing
rails new awesome_answers -d postgresql -T

# or if want to use react

rails new jscout-app --webpack=react
  • database connection info is stored in config/database.yml

  • start rails server

bin/rake db:create
bin/rails server
bin/rails s -p 3001
  • adding new gem requires restarting the server and run:
bundle install

##MVC

  • Model view controller

  • model interacts with the database

  • response to client is sent in html, css, javascript format

  • router decides which controller to use

  • routes.rb defines all url

# This will map any GET request with path '/hello' to welcomecontroller
# and index action within that controller

get "/hello" => "welcome#index"

-puts the higher priority routes at the top because the order of routes matters

  • in console: generate a controller
bin/rails g controller welcome
  • They generate a welcome_controller.rb in controller folder

  • Rails use a lot of meta programming

  • All controllers inherit from application_controller

  • in welcome_controller.index, define actions

class WelcomeController < ApplicationController
# we call instance methods defined in a controller 'actions'.
# we need actions in order to handle user requests

# below will let rail to automatically look for an index.html template

def index
end

end
  • in vews/welcome, create a welcome.index.erb file

####index

  • controller action

####html

  • format

####erb

  • templating system

####checks for routing

http://localhost:3000/rails/info/routes

or

bin/rake routes

steps for creating a new controller

  • in route
get "/subscribe" => "subscribe#index"
  • in application.html.erb
<%= link_to "Subscribe", subscribe_path %>
  • in command line
bin/rails g controller subscribe
  • go to the subscribe controller.rb
def index
# This renders the app/views/welcome/about.html.erb template with no layout
#    render "/welcome/about"
  end
  • go to views/subscribe, create a new file "index.html.erb"
<h1>subscribe</h1>

####form

post "/subscribe" => "subscribe#index"
  • index.html.erb under view/subscribe
<h1>Subscribe to our newsletter</h1>

<%# form_tag is a built-in rails view helper that enables us to
generate a form the first argument is the 'action' attirbute of the form.
you can provide method: attribute to make the form do non-POST request %>

<%= form_tag subscribe_path, method: :post do %>

<%end %>
  • in views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
  <title>AwesomeAnswers</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

<%# Hello_path and about_path are called route helpers.
they get generated from the content of 'routes.rb'
and they are methods that can be used in all view files and all controllers %>

<a href="<%= hello_path %>">Hello</a>
<a href="<%= about_path %>">About</a>
<%#  <a href="<%= gree_path(name: 'John') ">Greet John</a>%>
<a href="<%= greet_path('John') %>">Greet John</a>

<%# link_to is a helper method that is built-in with rails, it's a view helper method,
meaning it's accessible only in the view/view helpers. link_to
take a first argument which is the anchor text and a second argument which is
the 'href' value (url/path). It can take other options as we will see later
http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to %>
<%= link_to "Hello", hello_path %>
<%= link_to "Subscribe", subscribe_path %>

</body>
</html>
<input type="hidden" name="authenticity_token" value="szPlMvnERrIZcBChbIZVs3P5Xb3UDof+7fJmA/ZlmGCjlhF+rIQKe0wQv+5WVeNmFYatVhBzh3sCPWPv65lUBQ==">
  • go to the subscribe controller.rb
  def create
    render text: params
  end

####namespace

  • create our controller path
  • same view path
  • do need to define routes any more
namespace :admin do 
  resources :questions, :answers
end

scope

scope: "/admin" do
 resources :question
 end

Active Record - Object Relational Mapping

  • model (model name should be singular) maps to one table one row in a table (table name should be plural)
  • eg. Question maps to questions
bin/rails g model question title:string body:text
  • title attribute in string
  • body attribute in text

Model file

  • inherits ActiveRecord::Base
  • model maps something in the database

Migration file

  • inherits ActiveRecord::Migration
  • instruction we give to the database to create tables with the correct structure
  • timstamps will add two date time fields: created_at and updated_at
  • execute migration
bin/rake db:migrate
  • no need to specify ID column, ActiveRecord automatically creates an integer field called 'id' with AUTOINCREMENT

####pgAdmin

  • username for localhost is the username of the laptop (whoami)
  • if made mistake in schema, you can:
  • roll back if there is no data in the database yet
bin/rake db:rollback
bin/rake db:migrate
  • if there is data, create a new migration
bin/rails g migration add_view_count_to_questions
  • locate the migration file and add in your query
class AddViewCountToQuestions < ActiveRecord::Migration
  def change
    add_column :questions, :view_count, :integer
  end
end
  • execute migrate again
bin/rake db:migrate
  • adding rows in a table
bin/rails c

2.2.3 :001 > q = Question.new
+----+-------+------+------------+------------+------------+
| id | title | body | created_at | updated_at | view_count |
+----+-------+------+------------+------------+------------+
|    |       |      |            |            |            |
+----+-------+------+------------+------------+------------+
1 row in set
2.2.3 :002 > q.class
class Question < ActiveRecord::Base {
            :id => :integer,
         :title => :string,
          :body => :text,
    :created_at => :datetime,
    :updated_at => :datetime,
    :view_count => :integer
}
2.2.3 :003 > q.title= "My first question"
"My first question"
2.2.3 :004 > q
+----+-------------------+------+------------+------------+------------+
| id | title             | body | created_at | updated_at | view_count |
+----+-------------------+------+------------+------------+------------+
|    | My first question |      |            |            |            |
+----+-------------------+------+------------+------------+------------+
1 row in set

2.2.3 :016 > q.save
   (0.2ms)  BEGIN
  SQL (1.2ms)  INSERT INTO "questions" ("title", "body", "view_count", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["title", "My first question"], ["body", "my first question body"], ["view_count", 0], ["created_at", "2016-02-02 18:35:09.112873"], ["updated_at", "2016-02-02 18:35:09.112873"]]
   (1.3ms)  COMMIT

or

irb > q1=Question.new({title: "hello world!", body:"what's up!", view_count:1})
irb > q1.save
  • by default, rails wrap every query in database transaction (BEGIN, SQL, COMMIT)

  • view all questions (select * from)

Question.all
  • look for a row with a specified ID
Question.find 3
  • the find method will give u an ActiveRecord record not found exception if the condition is not valid

  • check the two ends of the table,

Question.first
Question.last
  • search by a specified column value. return nil if not found
 q = Question.find_by_title "my second question"
  Question Load (0.4ms)  SELECT  "questions".* FROM "questions" WHERE "questions"."title" = $1 LIMIT 1  [["title", "my second question"]]
+----+--------------------+-------------------------+-------------------------+-------------------------+------------+
| id | title              | body                    | created_at              | updated_at              | view_count |
+----+--------------------+-------------------------+-------------------------+-------------------------+------------+
| 3  | my second question | my second question body | 2016-02-02 18:46:48 UTC | 2016-02-02 18:46:48 UTC | 10         |
+----+--------------------+-------------------------+-------------------------+-------------------------+------------+
1 row in set
q = Question.find_by_title "my first question"
  Question Load (0.4ms)  SELECT  "questions".* FROM "questions" WHERE "questions"."title" = $1 LIMIT 1  [["title", "my first question"]]
nil
  • find_by_column* does not handle duplicated values; it will only return one record

  • if don't want to return all columns, can limit the returned values using select

Question.select(:id, :title).find_by_body "My first question body"
  • where
> Question.where({view_count: 0})
Question Load (0.3ms)  SELECT "questions".* FROM "questions" WHERE "questions"."view_count" = $1  [["view_count", 0]]
  
> Question.where("view_count > 0")
Question Load (0.4ms)  SELECT "questions".* FROM "questions" WHERE (view_count > 0)

> Question.where({view_count: 0}).class
Question::ActiveRecord_Relation < ActiveRecord::Relation
  • each
Question.where("view_count > 0").each {|x| puts x.title}
 Question Load (0.4ms)  SELECT "questions".* FROM "questions" WHERE (view_count > 0)
hello world!
my second question
+----+--------------------+-------------------------+-------------------------+-------------------------+------------+
| id | title              | body                    | created_at              | updated_at              | view_count |
+----+--------------------+-------------------------+-------------------------+-------------------------+------------+
| 2  | hello world!       | what's up!              | 2016-02-02 18:43:37 UTC | 2016-02-02 18:43:37 UTC | 1          |
| 3  | my second question | my second question body | 2016-02-02 18:46:48 UTC | 2016-02-02 18:46:48 UTC | 10         |
+----+--------------------+-------------------------+-------------------------+-------------------------+------------+
2 rows in set
  • all
 Question.where({view_count: 0, title:"abc"})
  Question Load (0.2ms)  SELECT "questions".* FROM "questions" WHERE "questions"."view_count" = $1 AND "questions"."title" = $2  [["view_count", 0], ["title", "abc"]]
[]
  • or needs us to write a sql query
  • where again
 Question.where(["created_at > ? OR view_count = 0", 5.days.ago])
  Question Load (0.6ms)  SELECT "questions".* FROM "questions" WHERE (created_at > '2016-01-28 19:04:45.821922')
+----+--------------------+-------------------------+-------------------------+-------------------------+------------+
| id | title              | body                    | created_at              | updated_at              | view_count |
+----+--------------------+-------------------------+-------------------------+-------------------------+------------+
| 1  | My first question  | my first question body  | 2016-02-02 18:35:09 UTC | 2016-02-02 18:35:09 UTC | 0          |
| 2  | hello world!       | what's up!              | 2016-02-02 18:43:37 UTC | 2016-02-02 18:43:37 UTC | 1          |
| 3  | my second question | my second question body | 2016-02-02 18:46:48 UTC | 2016-02-02 18:46:48 UTC | 10         |
+----+--------------------+-------------------------+-------------------------+-------------------------+------------+
  • search two conditions
Question.where(["title ILIKE ? OR body ILIKE ? ", "%my%", "%my%"])
  Question Load (0.5ms)  SELECT "questions".* FROM "questions" WHERE (title ILIKE '%my%' OR body ILIKE '%my%' )
  • create 100 records
100.times { Question.create(title: Faker::Company.bs, body: Faker::Lorem.paragraph, view_count: rand(10)) }
  • limit output, maybe also giving it an offset/frame
> Question.limit(10).offset(10)
  Question Load (0.5ms)  SELECT  "questions".* FROM "questions" LIMIT 10 OFFSET 10
  • update
q.update({body: "some new question body"})
   (0.2ms)  BEGIN
  SQL (0.4ms)  UPDATE "questions" SET "body" = $1, "updated_at" = $2 WHERE "questions"."id" = $3  [["body", "some new question body"], ["updated_at", "2016-02-02 19:38:35.821275"], ["id", 10]]
   (1.2ms)  COMMIT

destroy

q.destroy
   (0.2ms)  BEGIN
  SQL (0.2ms)  DELETE FROM "questions" WHERE "questions"."id" = $1  [["id", 10]]
   (1.3ms)  COMMIT

Index

  • create an index
bin/rails g migration add_indicies_to_questions
  • modify the migration file
class AddIndiciesToQuestions < ActiveRecord::Migration
  def change

    # This will add an index (not unique) to the queestiosn table on the title column
    add_index :questions, :title
    add_index :questions, :body

    # This will create a unique index
    # add_index :question, :body, unique: true

    # composite index
    # or add_index :questions, [:title, :body]


  end
end
  • can put ruby code in seeds.rb
100.times do
  Question.create title: Faker::Company.bs,
  body: Faker::Lorem.paragraph,
  view_count: rand(100)
end
bin/rake db:seed

Validation

  • put validations at the model level

  • avoid putting validations in the controller as much as possible

  • also can use jQuery to put in validation at the client level

  • edit the model

class Question < ActiveRecord::Base

  #validation
  #this will fail validations, so won't create or save,
  # if the title is not provided
  validates :title, presence: true

end
  • and
> reload!
> q = Question.new
q = Question.new
+----+-------+------+------------+------------+------------+
| id | title | body | created_at | updated_at | view_count |
+----+-------+------+------------+------------+------------+
|    |       |      |            |            |            |
+----+-------+------+------------+------------+------------+
1 row in set
2.2.3 :006 > q.save
   (0.2ms)  BEGIN
   (0.2ms)  ROLLBACK
false
> q.errors
{
    :title => [
        [0] "can't be blank"
    ]
}
2.2.3 :004 > q.errors.class
ActiveModel::Errors < Object

2.2.3 :005 > q.errors.full_messages
[
    [0] "Title can't be blank"
]
2.2.3 :006 > q.errors.full_messages.class
Array < Object
  • unique value
  validates :title, presence: true, uniqueness: true
> reload!
> q = Question.new(title: "My first question")
+----+-------------------+------+------------+------------+------------+
| id | title             | body | created_at | updated_at | view_count |
+----+-------------------+------+------------+------------+------------+
|    | My first question |      |            |            |            |
+----+-------------------+------+------------+------------+------------+
1 row in set
2.2.3 :010 > q.save
   (0.2ms)  BEGIN
  Question Exists (0.5ms)  SELECT  1 AS one FROM "questions" WHERE "questions"."title" = 'My first question' LIMIT 1
   (0.2ms)  ROLLBACK
false
  • numericality
  validates :view_count, numericality: {greater_than_or_equal_to: 0}
  • format
validates :email, format: {with: /\A([\w+\-]\.?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i}
  • customize valiation
  # Custom validation method. must make sure that
  #'no_monkey' is a method available for our class.
  # The method can be public or private.
  # it's more likely we will have it a private method because we
  # don't need to use it outside this class
  validate :no_monkey

  private

  def no_mokey
    if title.downcase.include? "monkey"
      errors.add(:title, "No monkey!!")
    end
  end
  • change error messages
  validates :body, uniqueness: {message: "must be unique!"}
q = Question.new(title: "title3333", body: "my first question body")
+----+-----------+------------------------+------------+------------+------------+
| id | title     | body                   | created_at | updated_at | view_count |
+----+-----------+------------------------+------------+------------+------------+
|    | title3333 | my first question body |            |            |            |
+----+-----------+------------------------+------------+------------+------------+
1 row in set
2.2.3 :013 > q.save
   (0.2ms)  BEGIN
  Question Exists (0.7ms)  SELECT  1 AS one FROM "questions" WHERE LOWER("questions"."title") = LOWER('title3333') LIMIT 1
  Question Exists (0.3ms)  SELECT  1 AS one FROM "questions" WHERE "questions"."body" = 'my first question body' LIMIT 1
   (0.2ms)  ROLLBACK
false
2.2.3 :014 > q.errors
{
    :body => [
        [0] "must be unique!"
    ]
}
> q.valid?
  Question Exists (0.4ms)  SELECT  1 AS one FROM "questions" WHERE "questions"."title" IS NULL LIMIT 1
  Question Exists (0.3ms)  SELECT  1 AS one FROM "questions" WHERE "questions"."body" IS NULL LIMIT 1
false

DSL

  • in model
  #DSL, domain specific language
# args
# :body = first arugment,
# {} hash with validation type as the field, and value could be any attribute
# the code we use in here is complete valid RUby code bu the method naming
# and arguments are specific to ActiveRecord so we call this an Active Record DSL
  validates( :body, {uniqueness: {message: "must be unique!"}})

callback

  • after_initialize
  • before_validation
  • after_validation
  • before_save
  • after_save
  • before_commit
  • fater_commit
  after_initialize :set_defaults
  before_save :capitalize_title

  private

  def set_defaults
    self.view_count ||= 0
  end

  def capitalize_title
    self.title.capitalize!
  end

Scope

  scope :recent, lambda{ order("created_at DESC").limit(5) }
  
  scope :created_after, lambda{|x| where("created_at > ? ", x) }
  
  #or
  # def self.recent
  #   order("created_at DESC").limit(5)
  # end
> Question.recent

Meta Programming

> method_name = "title"
> q = Question.new(title: "abc", view_count=10)
> q.sent(method_name)
"abc"

CRUD

  • create a new controller
  • in routes,
  get "/questions/new" => "questions#new", as: :new_question
  • define the new action in the controller file
class QuestionsController < ApplicationController

  def new
    @question = Question.new
  end

end
  • create a new erb, add a form_tag
<h1>New Question</h1>

<%# form_for takes in an activerecord object as a first argument, then it looks at the object. if the object is not persisted, the form will automatically use post for its method. it will also auto use 'questions_path' as its actions (convention is that the 'questions_path' will submit to the create action.)  %>

<%= form_for @question do |f| %>
<%# f is form object %>
<div>
<%= f.label :title %>
<%= f.text_field :title %>

</div>

<div>
  <%= f.label :body %>
  <%= f.text_area :body %>
</div>
<%end%>

<div>
  <%= f.submit %>
</div>
  • in routes, add
  post "/questions" => "questions#create", as: :questions
  • when submitting this form, the "create" action is not found thus error is thrown
class QuestionsController < ApplicationController

  def new
    @question = Question.new
  end

  def create
  
    # "question"=>{"title"=>"1", "body"=>"2"}
    # @question = Question.new
    # @question.title = params["question"]["title"]
    #
    # @question.body = params["question"]["body"]
    # @question.save


# permit methods will only allow parameters that are explicitly listed, in this case :title and :body
# strong parameters
question_params = params.require(:question).permit([:title, :body])

question = Question.new question_params
question.save
# render text: params
  end

end
  • params show the following hash
  • it separates the form values from the rest of the values (like authenticate tokens, etc)
"question"=>{"title"=>"1", "body"=>"2"}

render vs redirect

  • render
def create
    # permit methods will only allow parameters that are explicitly listed, in this case :title and :body
    question_params = params.require(:question).permit([:title, :body])

    question = Question.new question_params
    if   question.save
      render text: "Success"
    else
      # render is within the same request/response cycle

      # This will render app/views/questions/new.html.erb template

      # default behaviour is to render create.html.erb
      render :new
      # OR
      # render "question/new"
      # redirect makes a new request/response cycle
    end
  end
  • Redirect is favoured after creating a product since it changes the url
  # redirect_to question_path({id: @question.id})
  # redirect_to question_path({id:@question})
  # redirect_to @question
  redirect_to question_path(@question)
  • redirect_to will result in a 302 because 300 range is for redirect code

Link_to

  • get can re-use path from post
  post "/questions" => "questions#create", as: :questions
# The path for this url is the same as the path for the create action (with POST)
# so just use the one defined for create, which is :questions_path
  get "questions" => "questions#index"
  
  • home page
root "questions#index"
  • link to home page
<%= link_to "Home", root_path %>
  • Display records and enable hyperlink. Edit the template and the controller for question:
<h1>All Questions</h1>

<ol>
  <% @questions.each do |x| %>
  <li><%= link_to x.title, question_path(x) %></li>
  <% end %>
</ol>
  def index
    @questions = Question.all
  end

Actions

Create

  • in new.html.erb, give it a form
<h1>Create New Product</h1>

<%#= @product.errors.full_messages.join %>

<ul>
  <% @product.errors.full_messages.each do |m|  %>
  <li><%= m %></li>
  <% end %>
</ul>


<%= form_for @product do |f| %>
<%# f is form object %>
<div>
  <%= f.label :name %>
  <%= f.text_field :name %>

</div>

<div>
  <%= f.label :description %>
  <%= f.text_area :description %>
</div>

<div>
  <%= f.label :price %>
  <%= f.text_field :price %>
</div>

<div>
  <%= f.submit %>
</div>

<% end %>

####Edit

  • routes
get "/products/:id/edit" => "products#edit", as: :edit_question
patch "/products/:id" => "products#update" => "products#update"
  • create a new template
<%# form will use patch request if @product is persisted by adding hidden field
with name _method and value 'patch'.
Action URL = question_atph(@question) by convention
The form will prepopulate the fields with @question values %>
<%= form_for @product do |f| %>
<%# f is form object %>
<div>
  <%= f.label :name %>
  <%= f.text_field :name %>
...
the rest is the same as the form before
  • and in controller
 def update
    @product = Product.find params[:id]
    # strong param
    product_params = params.require(:product).permit([:name, :description, :price])
    # update automatically saves the record
    @product.update product_params
    redirect_to product_path(@product)
  end

####Destroy

  • in the show template where each individual product is displayed
<%= button_to "delete", @product, :method => :delete, data: {confirm: "Are you sure?"}%>
  • or
<%= link_to "Delete", product_path(@product), method: :delete, data: {confirm: "Are you sure?"}%>
  • in controller
      def destroy
      @product = Product.find params[:id]
      @product.destroy
      redirect_to root_path
    end

Refactoring

refactor using before actionn

before_action :find_question, only: [:show, :edit, :update]
# or
before_action :find_question, except: [:index]

refactoring using a "partial"

  • Partials have access to instance variables, eg. _form.html.erb.
  • you pass local variables by passing a hash to the partial as in
<%= render "form", my_var: my_variable %>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment