loading...
Cover image for Custom (Dynamic) Rails Routes via  Database Query

Custom (Dynamic) Rails Routes via Database Query

brad_beggs profile image Brad Beggs ・4 min read

For a recent proof-of-concept Rails project, a custom, dynamic, database created URL was required.

The requirements:

  1. allow a user to see all mountain biking and hiking trails filtered by state
  2. a bookmark-able (for quick access to the particular state) human-readable URL.

This walkthrough builds off Rubyonrails.org 3.2 Dynamic Segments with more details and support for use.

Assumptions:

  • You have a basic/entry-level working knowledge of Rails routes with resources helper method (e.g. resources :users, only: [:show, edit]).
  • You only need to get and not update, patch, post.
  • You have a database column with data suitable for making a url.
  • You read 3.2 Dynamic Segments and want a detailed example.

Step 1) Create the Route, aka the Dynamic Segments

Determine what your desired URL structure looks like.

We needed the url to look like /trails/state/wa, /trails/state/pa, or /trails/state/az.

The state abbreviation (e.g. WA, AZ, etc) are the url params[:state ] (similar to params[id]); the database contained consistent and searchable two letter state abbreviations. As such, the route in routes.rb is:

get "trails/state/:state", to: "trails#index", as: "trail_state"

Note: Place this route on the bottom of routes.rb so it does not impact other routes; it is the last route tested.

The route explained:

  • get trails/state/wa, to the trails#index action controller to execute the index.html.erb code.
  • trail_state is the helper path method to create the url (and the query!) for the unique state page. *For example, a link to all Washington trails:

<%= link_to "Washington Trails", trail_state_path("WA")%>

Step 2) Query the Database via Your Model

Your exact query will vary but what you query (the query string) is from your url params (in our case params[:state]).

The ActiveRecord query method is set in the trail.rb model as such:

def self.bystate(state:, amount: "10")
 # default is to pull 10 trails from DB, unless otherwise specified. This is optional. 
 Trail.joins(:park).where(parks: {state: state}).take(amount)
end

** Note on the model relationships for the curious:

  • trail.rb belongs_to :park
  • park.rb has_many :trails.
  • :state is a column in the trails table.
  • :park is the parks table.
  • take(10) pulls the first only the first 10 records for this proof of concept.

Now we have access to all (errr...10) records based on the query string set by the url.

Step 3) Display Data By Custom Query & URL.

First, create your user interface.

Depending on your needs, you could loop through your database to create the link_to or you can hardcode.

For the proof of concept, we hardcoded the states and an “All Trails” button on the trails index.html.erb page (below with CSS formatting removed):

<div >
    <h3><%= link_to "California Trails", trail_state_path("CA")%><h3>
    <h3><%= link_to "Washington Trails", trail_state_path("WA")%></h3>
    <h3><%= link_to "All Trails", trails_path%><h3>
</div>

On-screen a user clicks to see all trails or select state trails. The default index view is all trails via a normal get trails/ route.

Second, determine what to show based on url action.

To determine if we load a custom route and the resulting data query, check if the params[:state] is blank. If false (ie. no params[wa]), load all trails.

<%if !params[:state]%>
   <%# if there is no request for trails by state, show all the trails%>
  <% Trail.all.each do |trail|%>
    <%= render partial: "trails/trail", locals: {trail: trail}%>
  <%end%>
<%end%>

Otherwise, params[:state] is true and query the database and reload the index page with only selected state trails

<% Trail.bystate(state:params[:state]).each do |trail| %>
   <%= render partial: "trails/trail", locals: {trail: trail}%>
<%end%>

The Full Code Snippets

routes.rb
Rails.application.routes.draw do
  resources :parks, only: [:index, :show]
  resources :trails, only: [:index, :show]

root to: "homepage#index"

get "trails/state/:state", to: "trails#index", as: "trail_state"

end
trail.rb model
class Trail < ApplicationRecord
  belongs_to :park

def self.bystate(state:, amount: "10")
  Trail.joins(:park).where(parks: {state: state}).take(amount)
end
end
index.html.rb
<h2 class="font-weight-light text-center mt-5">See Trails by State</h2> 
<div class="row mx-md-n5 mt-5">
    <h3 class="font-weight-light text-center col px-md-3 d-flex justify-content-center"><%= link_to "California Trails", trail_state_path("CA")%><h3>
    <h3 class="font-weight-light text-center col px-md-3"><%= link_to "Washington Trails", trail_state_path("WA")%></h3>
    <h3 class="font-weight-light text-center col px-md-3 d-flex justify-content-center"><%= link_to "All Trails", trails_path%><h3>
</div>

<%# if there is a url request to show by state, show only that state%>
<div class="album">
   <div class="container-fluid text-center">
        <div class="d-flex justify-content-center row">
        <% Trail.bystate(state:params[:state]).each do |trail| %>
           <%= render partial: "trails/trail", locals: {trail: trail}%>
        <%end%>
    </div>
  </div>
</div>

<%if !params[:state]%>
<%# if there is no request for trails by state, show all the trails%>
    <div class="album font-weight-light">
    <div class="container-fluid text-center">
        <div class="d-flex justify-content-center row">
            <% Trail.all.each do |trail|%>
                <%= render partial: "trails/trail", locals: {trail: trail}%>
            <%end%>
            </div>
        </div>
    </div>
<%end%>

Posted on by:

brad_beggs profile

Brad Beggs

@brad_beggs

Curious and Persistent.

Discussion

markdown guide
 

Hey Brad, for your curiosity, here's how I would approach this.

First, I would use a scope instead of a class method and remove take from it to leave it chainable as is expected from a scope.
Take performs the query immediately and you will get an array of values instead of an object that can be chained with another query method (where, etc.).

class Trail < ApplicationRecord
  belongs_to :park
  scope :in_state, -> (state) { joins(:park).where(parks: { state: state }) }
end

There's no more pagination, but this should really be in the controller, just like the logic about the trails.
I would introduce a private method to filter trails and fall back on the default all scope if no state is provided.
I use limit instead of take to postpone any query and leave the @trails chainable in case we need to filter further down the road. I introduced an optional :per param that you can send along with your query to have a more flexible limit (eg. trail_state_path("CA", per: 100))
I would also save the selected state to use it in the view.

class TrailsController < ApplicationController
  def index
    @state = params[:state]
    @trails = filtered_trails || Trail.all
  end

  private
    def filtered_trails
      return unless params[:state].present?
      Trail.in_state(params[:state]).limit(params[:per].presence || 10)
    end
end

And now you don't need to have two blocks anymore for your list of trails in your index.
Also, you can use collection rendering instead of iterating.

<h2 class="font-weight-light text-center mt-5">See Trails by State</h2> 
<div class="row mx-md-n5 mt-5">
    <h3 class="font-weight-light text-center col px-md-3 d-flex justify-content-center"><%= link_to "California Trails", trail_state_path("CA")%><h3>
    <h3 class="font-weight-light text-center col px-md-3"><%= link_to "Washington Trails", trail_state_path("WA")%></h3>
    <h3 class="font-weight-light text-center col px-md-3 d-flex justify-content-center"><%= link_to "All Trails", trails_path%><h3>
</div>

<div class=(@state.present? ? "album" : "album font-weight-light")>
   <div class="container-fluid text-center">
        <div class="d-flex justify-content-center row">
          <%= render partial: "trails/trail", collection: @trails %>
        </div>
   </div>
</div>

You will probably want to make the currently selected state bolder, but I'll leave it there.

 

@arnaud Joubay Thank you for the time and suggestions. I'll remember the idea of keeping things as objects so one has the flexibility later. I like the refactor of removing the two blocks to show the states via collection rendering (a new concept for me).

 

You're welcome, glad you found my comment interesting.