概要
どのようなサービスか
長い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のページがうまく表示されているか確認します。
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を追加
Rails.application.routes.draw do
resources :urls
devise_for :users
+ root 'urls#index'
end
hash_valueはアプリケーション側で作成するので入力と表示を削除
<%= 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 %>
<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ができないようにします。
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
# 以下省略
ログアウトボタンを作成します。
<%# 省略 %>
<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のみを表示させます。また、ストロングパラメータも変更しておきます。
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
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
class Url < ApplicationRecord
+ belongs_to :user
end
今まで作成したURLは削除しておきます。
$ rails c
$ Url.delete_all
$ exit
これで違うユーザーでログインした場合にも、そのユーザーが投稿したURLのみが表示されます。
http://localhost:3000/にアクセスして複数ユーザーで投稿してみてください。
ここまでの差分
短縮URLの生成
URLのバリデーション
オリジナルのURLを投稿した際にURLとして適していない場合はバリデーションで弾くようにしましょう
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
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
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の表示
<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を追加
Rails.application.routes.draw do
resources :urls
devise_for :users
root 'urls#index'
+ get '/:hash_value', to: 'urls#redirect', as: 'redirect'
end
アクションを追加
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のアクセス数をグラフで表示する
- コピーボタンを作成する
(いいねをしてくださると追加する励みになります!)