Ruby on Rails lessons learned
- Setup application
 - Add tests with rspec to Rails
 - Controller’s actions naming
 - Form builders and routes helpers
 - Controller’s life cycle
 - Request params
 - Rendering views
 - Migrations helpers
 - Building forms
 - How to use Active Storage for local storage
 - How to reset db delete data and have schemas recreated
 - Databases SQLite3 and PostgreSQL
 - Turbo Rails
 
Setup application
- 
    
Setup rvm gemset first, read here:
 - 
    
Then rails:
 
gem install rails
rails new app-name
- Rails has 
bundlersupport baked in. It will be installed when you installrails. If you want you can use the binstubbin/bundle. 
Add tests with rspec to Rails
To your Gemfile, in the :development and :test groups, add:
group :development, :test do
  # ...
  gem "rspec-rails", "~> 6.0"
end
Install with bundle install.
Add rspec to your project:
bin/rails generate rspec:install
This generates:
create  .rspec
create  spec
create  spec/spec_helper.rb
create  spec/rails_helper.rb
rails_helper.rb: contains useful features for testing; it’s recommended to include it only in the spec files that require rails. It’s not loaded by default in.rspec.
Types of specs
- Integration tests that drive your app as a black box via its HTTP interface.
 - Functional tests to see how your controllers respond to requests.
 - Unit tests to drive a single object or layer.
 - Specific tests for models, mailers, and background jobs; any given test here may be a unit or integration test.
 
To test any of these aspects, tag your spec with type: <type>. The types provided by rspec-rails are:
Recommendations
- 
    
Some of these spec types, such as
:requestand:model, will be the bread and butter of your testing. Others are mainly there for edge cases or for backward compatibility, since rspec-rails works with all Rails versions from 3.0 up to the latest release. - 
    
Don’t feel pressed to include all types of tests in your app.
 - For outside-in acceptance testing:
    
- For HTTP-based APIs, use 
requestspecs. - For user-facing web applications, add Capybara to the project and use 
featurespecs; see Michael Crismali’s article for setup advice. 
 - For HTTP-based APIs, use 
 - For checking major components of your app:
    
- Use 
unitandintegrationspecs, without Rails where possible, for your domain objects. - Use 
model,mailer, andjobspecs for their respective types of Rails objects. 
 - Use 
 - Tend to avoid the following types of specs:
    
Viewspecs, which cost more effort than the value they provide; they encourage putting logic in your views, which we like to keep at a minimum.Routingspecs, which generally duplicate test coverage from your acceptance specs.Controllerspecs, which give an overly simplified picture of behavior, have some gotchas around how they bypass Rack middleware, and are being phased out of current Rails practice; userequestspecs instead.
 - The list of specs supported by rspec rails is not a checklist:
    
- Ask a hundred developers how to test an application, and you’ll get a hundred different answers.
 - RSpec Rails provides thoughtfully selected features to encourage good testing practices, but there’s no “right” way to do it. Ultimately, it’s up to you to decide how your test suite will be composed.
 
 - Don’t use 
render_templateorassert_templatein your specs. These were deprecated and they indicate a code smell.- Don’t include the gem 
rails-controller-testingin your Gemfile. These features were externalized to this gem. - Instead test for something different: status code, database changes, response content.
 
 - Don’t include the gem 
 
Useful commands
Rebuild test database:
bin/rails db:test:prepare
Adding specs
Add specs for a model:
bin/rails generate rspec:model ModelName
Add requests specs (integration):
bin/rails generate rspec:request Video
Controller’s actions naming
Never create a controller’s action with a name that already exists in the Base controller. For example:
def process
  ...
end
You will get an error like: Wrong number of arguments(given 1, expected 0).
Form builders and routes helpers
- 
    
The form builder
form_with model: @some_modelrequires the model to have apostroute to/some_model. Otherwise you’ll get the errorsome_model_pathmethod not found. - 
    
How to make the
some_model_urlhelper available?Naming routes:
- 
        
adding
asas keyword argument to your route will generate helper methods for your route:get "videos/:id", to: "videos#show", as: :videowill generate
video_pathandvideo_urlhelpers.Docs: https://guides.rubyonrails.org/routing.html#naming-routes
 
 - 
        
 
Controller’s life cycle
A controller instance is created per request.
Request params
- Rails doesn’t make a distinction between query params and POST params, all go inside the 
paramshash. - Parameters in 
paramsare alwaysstrings, Rails doesn’t try to cast or guess the data type. 
Rendering views
- 
    
Why is it required to instantiate the model when rendering a view (for example the
newview)?- TODO.
 
 - Rendering a view using 
renderwill NOT call the action associated to the view, therefore:- You have to define the instance variables used by the view.
 
 - Rendering or redirecting won’t stop the action, expressions after the call to rendering will be evaluated.
    
- Whatchout, you can’t 
render/redirecttwice. 
 - Whatchout, you can’t 
 - What is record identification? https://guides.rubyonrails.org/form_helpers.html#relying-on-record-identification
 
Migrations helpers
- 
    
Update schema:
# Add multiple columns to table. bin/rails generate migration AddNameToVideosAndSizeToVideos name:string size:string - 
    
To change a column from nullable to non-null you have to do it manually:
- Open the migration file that corresponds to the table creation (or column addition)
 - 
        
Modify the required column by adding the keyword argument
null: false. For example:# from t.string :name # to t.string :name, null: false 
 - 
    
Create one-to-many association in already existing models:
- 
        
You’ll have to create the migration code manually:
- 
            
Create an empty migration file:
bin/rails g migration AddOwnerTableFkToOwnedTable - Where 
OwnerTableis the table thathas_manyOwnedTable. - This will generate for you an empty migration file with the proper timestamp.
 
 - 
            
 - 
        
Edit the
changemethod and add:add_reference :owned_table_in_plural, :owner_table, foreign_key: true, null: false - 
        
Run migration:
bin/rails db:migrate - 
        
Edit the model files to add associations:
# Owner.rb class Owner < ApplicationRecord ... has_many :owned_table_in_plural, dependent: :destroy ... end # Owned.rb class Owned < ApplicationRecord ... belongs_to :owner ... end - 
        
Add/delete data:
@owner = Owner.find @owner.owneds.create(...) @owner.destroy # will delete the objects it has too. 
 - 
        
 - 
    
Running
rollbackwill rollback only one migration. 
Building forms
When building a form input field passing a symbol as parameter, that symbol does not have to be
defined in the model. It can be anything. Using an attribute that’s in the model will be helpful
to avoid boilerplate code when extracting from params in the controller. Example:
<%= form_with model: @video do |form| %>
<%= form.file_field :video_file %>
<%= form.submit %>
<% end %>
video_file doesn’t have to be an attribute in @video.
How to use Active Storage for local storage
- 
    
Set up:
bin/rails active_storage:install bin/rails db:migrateThis will create three tables in your db:
active_storage_blobs,active_storage_attachments, andactive_storage_variant_records. - 
    
Set storage service. Edit file
config/storage.yml:local: service: Disk root: <%= Rails.root.join("storage") %> - 
    
Tell active storage which service to use. Edit
config/environment.rb:Rails.application.config.active_storage.service = :local - 
    
Attach files to records. Example:
A
Videorecord with manyImages records, and eachImagewith one file attached:Define the associations:
class Video < ApplicationRecord has_many :images, dependent: :destroy end class Image < ApplicationRecord has_one_attached :image_file # bin/rails g model Image image_file:attachment belongs_to :video endCreate the migrations. Refer to the section on creating a foreign key in migrations helpers.
Attach files:
@video = Video.create @image = @video.images.create # For attaching a file created in the server: @image.image_file.attach( io: File.open(file_path), filename: "somename.jpg", content_type: "image/jpeg", identify: false ) # For attaching a file uploaded through the request: # Read here: https://edgeguides.rubyonrails.org/active_storage_overview.html#has-one-attachedDelete files:
# This will delete all images and their attachments too. @video.destroy 
Docs: https://edgeguides.rubyonrails.org/active_storage_overview.html
Example project: https://github.com/hamax97/coordinates-reader
How to reset db (delete data and have schemas recreated)
bin/rails db:reset
Databases (SQLite3 and PostgreSQL)
- 
    
SQLite3 fails with:
SQLite3::BusyException: database is lockedwhen using Active Storage to store a relatively big amount of images, for instance, about 60 images sequentially.- Seems like Active Storage makes two or three 
SELECTs beforeINSERT, per image, which might cause this. - When using PostgreSQL, this issue is fixed.
 
 - Seems like Active Storage makes two or three 
 - 
    
Things to have in mind when setting up PostgreSQL:
- Default role used? https://stackoverflow.com/questions/24038316/rails-connects-to-database-without-username-or-password
 - 
        
The option
hostmust be defined if usingconfig/database.yml, otherwise you won’t be able to connect, with an error that doesn’t specify that thehostoption should be set, something like:ActiveRecord::DatabaseConnectionError: There is an issue connecting to your database with your username/password, username: <username>. Please check your database configuration to ensure the username/password are valid. 
 - 
    
Useful commands:
bin/rails db:drop # delete databases for all envs bin/rails db:truncate_all # truncate all tables 
Turbo Rails
- When using Turbo, all clicks in 
<a></a>tags are intercepted and instead of a full page render, Turbo will usefetch()and replace the current HTML with the fetched HTML. Therefore, if you have any JavaScript file referenced in your HTML, it will NOT be executed again, it’s only executed when there’s a full page load. If there’s any inline JavaScript, it WILL be executed as expected.