3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GeekSalonAdvent Calendar 2023

Day 24

RailsでURL短縮サービスをつくってみる

Last updated at Posted at 2023-12-23

概要

どのようなサービスか

長いURLを短いURLに変換するサービスです。
有名どころで言うとbitlyですね。
試しに、Wikipediaの長いURLを変換してみます
長いURL:https://ja.wikipedia.org/wiki/1-%E3%83%94%E3%83%AD%E3%83%AA%E3%83%B3-4-%E3%83%92%E3%83%89%E3%83%AD%E3%82%AD%E3%82%B7-2-%E3%82%AB%E3%83%AB%E3%83%9C%E3%83%B3%E9%85%B8%E3%83%87%E3%82%A2%E3%83%9F%E3%83%8A%E3%83%BC%E3%82%BC
短いURL:https://bit.ly/49UA01h
どちらも同じページへ遷移することが確認できます。

短縮方法

URL短縮用のGemもありますが、今回は使用せずにハッシュ変換+衝突判定で開発します。

リポジトリ

コードを確認したい方は以下へ

開発環境

$ ruby -v
ruby 3.0.4p208 (2022-04-12 revision 3fa771dded) [x86_64-linux]
$ rails -v
Rails 7.0.8

Rails new

$ rails new url-shortener --css tailwind
$ cd url-shortener
$ bin/dev

http://localhost:3000/にアクセスしてRailsのページがうまく表示されているか確認します。
image.png

deviseの導入

deviseをつかってログイン機能を実装します。
公式リポジトリに沿って実装していきます。

$ bundle add devise
$ rails generate devise:install
$ rails generate devise User
$ rails db:migrate

http://localhost:3000/users/sign_upにてサインアップができるか試してみてください。

ここまでの差分

管理画面の作成

URLをCRUDできるようにする

$ rails g scaffold Url original hash_value
$ rails db:migrate

root_urlを追加

config/routes.rb
Rails.application.routes.draw do
  resources :urls
  devise_for :users
+ root 'urls#index'
end

hash_valueはアプリケーション側で作成するので入力と表示を削除

app/views/urls/_form.html.erb
<%= form_with(model: url, class: "contents") do |form| %>
  <% if url.errors.any? %>
    <div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3">
      <h2><%= pluralize(url.errors.count, "error") %> prohibited this url from being saved:</h2>

      <ul>
        <% url.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="my-5">
    <%= form.label :original %>
    <%= form.text_field :original, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
  </div>

- <div class="my-5">
-   <%= form.label :hash_value %>
-   <%= form.text_field :hash_value, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
- </div>

  <div class="inline">
    <%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
  </div>
<% end %>
app/views/urls/_url.html.erb
<div id="<%= dom_id url %>">
  <p class="my-5">
    <strong class="block font-medium mb-1">Original:</strong>
    <%= url.original %>
  </p>

- <p class="my-5">
-   <strong class="block font-medium mb-1">Hash value:</strong>
-   <%= url.hash_value %>
- </p>

  <% if action_name != "show" %>
    <%= link_to "Show this url", url, class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
    <%= link_to "Edit this url", edit_url_path(url), class: "rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium" %>
    <hr class="mt-6">
  <% end %>
</div>

http://localhost:3000/にアクセスしてURLを作成・表示・編集・削除ができるか試してみてください。
ここまでの変更

ログイン制限

ログインしないとURLのCRUDができないようにします。

app/controllers/urls_controller.rb
class UrlsController < ApplicationController
  before_action :set_url, only: %i[ show edit update destroy ]
+ before_action :authenticate_user!

  # GET /urls or /urls.json
  def index
    @urls = Url.all
  end

  # GET /urls/1 or /urls/1.json
  def show
  end
  # 以下省略

ログアウトボタンを作成します。

app/views/layouts/application.html.erb
<%# 省略 %>
  <body>
    <main class="container mx-auto mt-28 px-5 flex">
+     <% if user_signed_in? %>
+       <%= button_to 'ログアウト', destroy_user_session_path, method: :delete, class: "text-white bg-red-500 hover:bg-red-700 font-medium py-2 px-4 rounded" %>
+     <% end %>

      <%= yield %>
    </main>
  </body>
</html>

自分が作成したURLのみを表示させます。また、ストロングパラメータも変更しておきます。

app/controllers/urls_controller.rb
class UrlsController < ApplicationController
  before_action :set_url, only: %i[ show edit update destroy ]
  before_action :authenticate_user!

  # GET /urls or /urls.json
  def index
-   @urls = Url.all
+   @urls = current_user.urls
  end

  # GET /urls/1 or /urls/1.json
  def show
  end
  
# 省略
  # POST /urls or /urls.json
  def create
-   @url = Url.new(url_params)
+   @url = current_user.urls.new(url_params)

    respond_to do |format|
      if @url.save
 # 省略
  private
    # Use callbacks to share common setup or constraints between actions.
    def set_url
-     @url = Url.find(params[:id])
+     @url = current_user.urls.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def url_params
-     params.require(:url).permit(:original, :hash_value)
+     params.require(:url).permit(:original)
    end
end

アソシエーションを追加します。

$ rails generate migration AddUserToUrls user:references
$ rails db:migrate
app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
+ has_many :urls
end
app/models/url.rb
class Url < ApplicationRecord
+ belongs_to :user
end

今まで作成したURLは削除しておきます。

$ rails c
$ Url.delete_all
$ exit

これで違うユーザーでログインした場合にも、そのユーザーが投稿したURLのみが表示されます。
http://localhost:3000/にアクセスして複数ユーザーで投稿してみてください。

ここまでの差分

短縮URLの生成

URLのバリデーション

オリジナルのURLを投稿した際にURLとして適していない場合はバリデーションで弾くようにしましょう

app/models/url.rb
class Url < ApplicationRecord
  belongs_to :user
+ validates_format_of :original, with: /\A(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*|%[0-9a-fA-F]{2,})*\/?\z/
end

正規表現を使ってバリデーションを作ることで、でURLのみ投稿できるようになりました。

ハッシュ変換

長いURLを短いURLに変換する際に、短いURLは最低でもどれくらい必要でしょうか。
数字とアルファベットのみで短いURLを表すとする場合62文字使用できます。
62^1=62
62^2=3,844
62^3=238,328
.
.
.
62^7=3,521,614,606,208
となるので、7桁あれば、3.5兆のレコードに対応できるので十分でしょう。
そして、ハッシュ関数をつかってオリジナルURLをハッシュ化し、その最初の7桁を取得し、衝突判定を行いDBで重複していないことを確認してから、hash_valueに保存するという実装にします。

class Url < ApplicationRecord
  belongs_to :user
  validates_format_of :original, with: /\A(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*|%[0-9a-fA-F]{2,})*\/?\z/
  validates :hash_value, uniqueness: true

+ before_create :generate_hash_value
+
+ private
+
+ def generate_hash_value
+   self.hash_value = create_unique_hash_value
+ end
+
+ def create_unique_hash_value
+   loop do
+     hash = Digest::SHA256.hexdigest(original)[0...7]
+     break hash unless Url.exists?(hash_value: hash)
+   end
+ end
end

indexを張る

hash_valueカラムの検索パフォーマンス向上のためにindexをつけます。

$ rails generate migration AddIndexToHashValue
db/migrate/xxxxxxxxxxxxxxx_add_index_to_hash_value.rb
class AddIndexToHashValue < ActiveRecord::Migration[7.0]
  def change
+   add_index :urls, :hash_value, unique: true
  end
end

ユニーク制限

hash_valueが重複しないように、バリデーションとDBでユニーク制限をつけておきます。

class Url < ApplicationRecord
  belongs_to :user
  validates_format_of :original, with: /\A(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*|%[0-9a-fA-F]{2,})*\/?\z/
+ validates :hash_value, uniqueness: true
# 省略
end
db/migrate/xxxxxxxxxxxxxxx_add_index_to_hash_value.rb
class AddIndexToHashValue < ActiveRecord::Migration[7.0]
  def change
    add_index :urls, :hash_value, unique: true # 先程追加したコードで実装済み
  end
end
$ rails db:migrate

これによって、短いURLに使用するhash_valueを作成できるようになりました。
ここまでの差分

短縮URLにアクセスされた際のリダイレクト処理

短縮URLの表示

app/views/urls/_url.html.erb
<div id="<%= dom_id url %>">
  <p class="my-5">
    <strong class="block font-medium mb-1">Original:</strong>
    <%= url.original %>
  </p>

+ <p class="my-5">
+   <strong class="block font-medium mb-1">Short url:</strong>
+   <%= root_url + url.hash_value %>
+ </p>
<%# 省略 %>
</div>

リダイレクト処理

routesを追加

config/routes.rb
Rails.application.routes.draw do
  resources :urls
  devise_for :users
  root 'urls#index'
+ get '/:hash_value', to: 'urls#redirect', as: 'redirect'
end

アクションを追加

app/controllers/urls_controller.rb
class UrlsController < ApplicationController
#省略
+ def redirect
+   url = Url.find_by(hash_value: params[:hash_value])
+   redirect_to url.original, allow_other_host: true
+ end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_url
      @url = current_user.urls.find(params[:id])
    end
#省略
end

これで、長いURLを短いURLに変換し、短いURLにアクセスされた際は長いURLへ遷移させることができました。

ここまでの差分

まとめ

以上で基本機能は完成です。
もし、今後時間があれば、以下の機能も追加できればと思います。

  • 管理画面にて、短縮URLのアクセス数をグラフで表示する
  • コピーボタンを作成する

(いいねをしてくださると追加する励みになります!)

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?