ryota21silvaの技術ブログ

Funna(ふんな)の技術ブログ

これまで学んだ技術の備忘録。未来の自分が救われることを信じて

リストレンダリング(v-for)

v-forで配列に要素をマッピング

配列に基づいて、アイテムのリストを描画するために、v-for ディレクティブを使用することができます。v-for ディレクティブは item in items の形式で特別な構文を要求し、items はソースデータの配列で、item は配列要素がその上で反復されているエイリアスです(Vue.js公式)

<ul id="example-1">
  <li v-for="item in items" :key="item.message">
    {{ item.message }}
  </li>
</ul>
var example1 = new Vue({
  el: '#example-1',
  data: {
    items: [
      { message: 'Foo' },
      { message: 'Bar' }
    ]
  }
})

結果

  • Foo
  • Bar

オブジェクトのv-for

オブジェクトのプロパティに対しても、v-for を使って反復処理ができる。
以下ではオブジェクトのvalueを描画している。

<script src="https://cdn.jsdelivr.net/npm/vue"></script>

<ul id="app">
  <li v-for="value in object">
    {{ value }}
  </li>
</ul>
<script>
  const vm = new Vue({
    el: "#app",
    data: {
      object: {
        name: 'Ryota',
        age: 23,
        gender: 'Male'
      }
    }
  })
  
  // 実際に描画されるHTMLを確認
  console.log(vm.$el)  
</script>

2つ目の引数として、プロパティ(=key)も描画できる

<div v-for="(value, name) in object">
  {{ name }}: {{ value }}
</div>

結果

  • name: 'Ryota'
  • age: 23
  • gender: 'Male'

v-ifとv-showの違いとは ~条件付きレンダリング~

v-show

<タグ名 v-show="条件式">trueなら表示</タグ名>

v-ifとv-showの比較

表示、非表示の動作が異なる。

  • v-ifはDOMレベルで要素が削除され、消えた部分はコメント化される。
  • v-showはDOMから要素は削除されず、dispay: none;が設定される。
    →DOMへの要素の生成と削除は負担が大きいため、上手く使い分ける必要があるみたい。

v-if

切り替えコストが高い(DOM要素も表示、非表示させるから)
初期描画コストが低い
内部の状態は初期化される

v-show

切り替えコストが低い(CSSレベル)
初期描画コストが高い(必ず描画される)
内部の状態は保持される

v-ifとv-showの書き方

<script src="https://cdn.jsdelivr.net/npm/vue"></script>

<div id="app">
  <div v-if="true">v-if="true"のとき</div>
  <div v-if="false">v-if="false"のとき</div>
  <div v-show="true">v-show="true"のとき</div>
  <div v-show="false">v-show="false"のとき</div>
</div>
<script>
  new Vue({ el: '#app' })
   // 実際に描画されるHTML
   const el = document.getElementById('app')
   console.log(el.innerHTML.replace(/> </g, '>\n<'))
</script>

false時のコンソールの内容が異なる

# 検証ツールなど
<div>v-if="true"のとき</div>
<!---->

<div>v-show="true"のとき</div>
<div style="display: none;">v-show="false"のとき</div>

Vueの算出プロパティ(computed)、監視プロパティとの比較

computedとは

関数で算出したデータを返すことができるプロパティ(getterということ)

  • データの加工を伴った取得ができる。
  • キャッシュが残るから、処理が早くなる。
computed: { 算出プロパティ: 関数のオブジェクト }

fullName: function() {  return${this.firstName} ${this.lastName}  }
→dataオブジェクトとバインドする
<li>{{ fullName }}</li>にリアルタイムで表示される

<script src="https://cdn.jsdelivr.net/npm/vue"></script>

<ul id="app">
  <li>{{ firstName }}</li>
  <li>{{ lastName }}</li>
  <li>{{ fullName }}</li>
</ul>
<script>
  new Vue({
    el: '#app',
   // dataオブジェクト
    data: {
      firstName: 'Taro',
      lastName: 'Yamada'
    },
   // computed: { fullName: function()... }
    computed: {
      fullName: function() {
        return `${this.firstName} ${this.lastName}`
      }
    }
  })
</script>

lastNameの値を書き換えると、リアルタイムでfullNameが評価される。

<script src="https://cdn.jsdelivr.net/npm/vue"></script>

<div id="app">
  <input v-model="firstName">

  <input v-model="lastName">

  <span>{{ fullName }}</span>
</div>
<script>
  new Vue({
    el: '#app',
    data: {
      firstName: 'Taro',
      lastName: 'Yamada'
    },
    computed: {
      fullName: function () {
        return `${this.lastName} ${this.firstName}`
      }
    }
  })
</script>

監視プロパテとの比較

基本的な動きは算出プロパティと同じのため、コードの見やすさ的に算出プロパティを使えば良さそう。

監視プロパティは非同期通信が行える。

watch オプションを利用することで、非同期処理( API のアクセス)の実行や、処理をどのくらいの頻度で実行するかを制御したり、最終的な answer が取得できるまでは中間の状態にしておく、といったことが可能になっています。これらはいずれも算出プロパティでは実現できません。(Vue.js公式より)

watch: { 監視プロパティ: 関数のオブジェクト }
watch: {
  data() {
    return {
      処理
    }
  },
  sample: function() {
    処理
  }
}

Ajax

ウェブアプリにデータを反映する手法で、Webサーバーから非同期処理でXMLデータを取得して、コンテンツに反映できる。DOMと相性良いらしい。

DOM

Document Object Model (DOM) は、HTML および XML ドキュメントのための API 。これはドキュメントの構造的な表現を提供し、内容や表示形態の変更を可能にする。Web ページをスクリプトプログラミング言語とつなぐような機構ということ。 (MDNより)

→つまり、Ajaxを使って動的なWebページを作成するときに、HTML・XML上のどの要素を変更するかを指定する。そこでDOMはHTMLやXMLを「ツリー構造」として展開し、アプリケーション側に文章の情報を伝え、加工や変更をしやすくしてくれる。
ツリー構造とは、データ構造の一つで、一つの要素(ノード)が複数の子要素を持ち、一つの子要素が複数の孫要素を持ち、という形で階層が深くなるほど枝分かれしていく構造のこと。

XML

XMLはタグを自由に設定でき、そのタグに意味づけをすることができる。
データのやりとりで「XML」を使えば、複数のデータを同時にやりとりしても、どのデータがどの要素なのか一発で判明できます。
ただ現在では、AjaxにはXMLの代わりにJSONという型がよく使われています。Ajaxという名前にXMLが入っているので、他の型は使えないように思いますが、そうではないんですね。

Json

JavaScript Object Notationの略。
軽量のデータ交換フォーマットで、人間にとって読み書きが容易で、マシンにとっても簡単にパースや生成を行なえる形式。

axios

非同期処理の機能を持つPromiseというライブラリを基にした方法

Vueのデータバインディングとか基礎的なのをまとめてみた(v-bind, v-on, v-for, v-model, componentとか)

Vueのデータバインディング


追記(6/15)

  • バインディングとは、データとUIを結びつけること
  • 双方向とは、データを更新⇄UIを更新
  • dataで、データを保持させる
    name(プロパティ)とvalueのセット。
  • {{ }}にはjsの式も書ける

  • new VueはVue.jsの心臓部分みたいなもの。
  • Vueインスタンスelで、Vueがどの部分のテンプレートを扱うか判断してくれる(VueインスタンスをDOM要素(HTMLとか)に紐付ける)
    el: '#app'<div id='app'>に紐付く。
  • マスタッシュ構文{{ 〇〇 }}を使うことで、テンプレート内のデータオブジェクトにアクセスできる。
    →{{ }}内にはJavascriptの記述ができる。{{ name.toUpperCase }}とか。独自メソッドも記述できる。

  • {{ 〇〇 }}
    → dataのmessageにアクセス
    →dataのmessageの値がマスタッシュ構文内のmessageに代入される。
    {{ data.message }}ではなく{ message }と記述することに注意。

<div id='app'>
    {{ message }}
</div>

<script>
 new Vue({
   el: '#app',
   data: {
     message: 'Hello World!'
   }
 })
</script>

当たり前だけど、以下はHTMLではなくテンプレート(v-forとかもテンプレート)を書いている。そしてVueがテンプレートの記述を見て「あ、これはテンプレート構文だ!」と判断して、HTMLに変換して出力してくれる。それをブラウザが受け取って画面に表示してくれるって感じ。

<div id='app'>
  {{ message }}
</div>

データバインディングとは?

  • データと出力(描画)を同期する仕組み

  • 「v-」から始まるディレクティブ(命令)

v-bind

モジュールの値をコンポーネントに出力する。HTML要素のデータをバインドできる。
v-bind:引数="js側のプロパティ"という形で記述する。引数にはinputタグならvalue、aタグならhrefとか、タグにあった属性を記述する。
:引数="js側のプロパティ"に省略可能。

<div id="app">
  <input :value="message">
</div>

<script>
  new Vue({
    el: '#app',
        data: {
          message: 'Hello World!'
        }
  })

</script>

>> Hello World!

他にも[ ]を使った記述もできる。
Hello World!というリンクが表示される。

<div id="app">
  <!-- <a href='https:google.com'> と同じ --!>
  <a :[attribute]="url">Twitter</a>
</div>
<script>
  new Vue({
    el: '#app',
        data: {
          url: 'https://google.com',
          attribute: 'href'
        }
  })
</script>
>> Hello World!

更にv-bindでバインドする内容をオブジェクト形式で記述できる。

<div id="app">
  <!-- <a href='https://twitter.com', id=21> と同じ --!>
  <a v-bind="{href: urlTwitter, id: number}">Twitter</a>
</div>
<script>
  new Vue({
    el: '#app',
        data: {
          urlTwitter: 'https://twitter.com',
          number: 21
        }
  })
</script>

更にVue側にオブジェクトの中身を記述することもできる。

<div id="app">
  <!-- <a href='https://twitter.com', id=21> と同じ --!>
  <a v-bind="twitterObject">Twitter</a>
</div>
<script>
  new Vue({
    el: '#app',
        data: {
           twitterOblect: {
             href: 'https://twitter.com',
             id: 21
        }
  })
</script>

v-ifディレクティブ

HTMLの要素を表示、非表示
→非表示の時、要素ごと消える(DOMレベルで削除)
→表示で要素を再構築

<div id="app">
  <h1 v-if="flag">Vue is Good!</h1>
  <h1 v-else>Vue is Bad!</h1>
</div>
<script>
  new Vue({
    el: '#app',
    data: {
      flag: true
    }
  })
</script>

v-forディレクティブ

ループ処理

<タグ名 v-for="変数名 in 配列名"> {{ 変数名 }} </タグ名>

data内に記述した配列の要素が{{ item }}に代入される。 data内の配列がelによって#appと紐づいて、v-forでdata内の要素が順にitemに代入されるって感じ。

<ul id="app">
  <li v-for="item in list">{{ item }}</li>
</ul>

<script>
  new Vue({
    el: '#app',
        data: {
          list: ['Vue', 'React', 'Angular']
        }
  })
</script>

v-on

v-on:click@clickに省略できる

<タグ名 v-on: イベント名 = "ハンドラ名"> 表示したい文字 </タグ名>

methods: {
  ハンドラ名: 関数名() {
    処理
  }
}

これでクリックするたびに数値がカウントされていく

<div id="app">
  <span>{{ count }}</span>
  <button @click="increment">+</button>  ← +ボタン
</div>
  
<script>
  new Vue({
    el: '#app',
        data: {
          count: 0
        },
        methods: {
          increment: function() {
            this.count++
          }
        }
  })
</script>

ついでに

双方向バインディングとは

v-bindだと、HTML側の値を変更したとしても、モジュールの値は変更されない。

そこで双方向バインディングを使えば、プレビュー(HTML)側で入力した内容が、モジュール側の値を変更してくれる。
→入力内容を変更したことでデータの更新がなされ、DOMに反映される。

v-model ディレクティブ

v-bindとv-onの2つの処理を組み合わせたもの。
- inputにv-model="message"
→inputのフォームに入力した値が{{ message }}に結びつく。

<タグ名 v-model = "プロパティ名">
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

<div id="app">
  <p>{{message}}</p>
  <input v-model="message">
</div>

<script>
  new Vue({
    el: '#app',
    data: {
      message: 'Hello World!'
    }
  })
</script>

<input v-model="message">data: { message: 'Hello World!' }Hello worldが入ってて、{{message}}にHello worldが表示される。
→inputを書き換えると{{message}}も書き換わる

component

Webサイトの画面はヘッダー、メイン、フッター、サイドバーなど色々な部品から構成される。 コンポーネントを使えば、部品ごとにHTML, CSS, JSを1セットにできる。
→変更箇所の発見、メンテナンスが容易になる、

Vue.component('コンポーネント名', {コンポーネントのオブジェクト})

Vue.component('my-component', { template: '<li>Hello World!</li>'})

コンポーネントは再利用が可能

<script src="https://cdn.jsdelivr.net/npm/vue"></script>

<div id="app">
    <my-component></my-component>
    <my-component></my-component>
    <my-component></my-component>
</div>

<script>
  Vue.component('my-component', {
    template: '<li>Hello World!</li>'
  })
  
  new Vue({
    el: '#app'
  })
</script>

>>
・ Hello World!
・ Hello World!
・ Hello World!

コンポーネントの親子間のデータの受け渡し

<div id="app">
  <my-component :message="'hoge'"></my-component>
  <my-component :message="'fuga'"></my-component>
  <my-component :message="'hogefuga'"></my-component>
</div>

<script>
  Vue.component('my-component', {
    props: ['message'],
    template: '<li>{{ message }} </li>'
  })
  
  new Vue({
    el: '#app'
  })
</script>


>> 
・hoge
・fuga
・hogefuga

クラスの継承(単一テーブル継承)

クラスの継承

あるクラスを継承して、新たにクラスを作成すること。
よくある例だと、Animalクラスとそれを継承するDogクラス、Catクラスみたいな。

クラスの継承を行うことで、継承元の親クラス(スーパークラス)に定義された機能を、子クラス(サブクラス)でも利用できる。
つまり、あるメソッドを複数のクラスで使用したい、同じカラムを複数のクラスで使用したいという時に継承を使うと便利です。

単一テーブル継承(STI)とは

上記で説明したようなクラスの継承関係を、DB上のたった1つのテーブルで実現したもの。
つまり、各モデルが持っている情報は同じだから、それらを1つのテーブルでまとめて扱おうってのがSTI

(単一テーブル継承とは、共通の処理が纏められたスーパークラスと、そのスーパークラスを継承しつつ、各々特有の処理が記述されたサブクラスというクラス郡の親子関係を、1つのテーブルだけで実現したもの。 簡単に言うと、DBにある1つのテーブルを、複数のモデルで共有利用すること。)


テーブル設計の要件
  • 記事テーブルにタグ、カテゴリー、著者といったテーブルを関連付けしたい。
  • タグ、カテゴリー、著者は同じようなメソッドを使う、同じカラムを使用する。

最終的に以下のようなテーブル設計になる。

f:id:ryota21silva:20200609195822p:plain
STI


実装

まずは継承元となるTaxonomyモデルを作成し、かつ、DB上にTaxonomiesテーブルも反映させる。(ここではモデルの作成、マイグレーションファイルの反映などの説明は省略する)

上記の結果、スキーマファイルにはTaxonomiesテーブルが反映されている。
STIの説明で言及したたった1つのテーブルというのがTaxnomiesテーブルのことで、このテーブルを使ってSTIという継承関係を実現する。

スキーマにTaxonomiesテーブルが反映されている。
(db/schema.rb)
create_table "taxonomies", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC", force: :cascade do |t|
    t.string "type"
    t.string "name"
    t.string "slug"
    t.text "description"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["slug"], name: "index_taxonomies_on_slug"
    t.index ["type"], name: "index_taxonomies_on_type"
  end
Taxonomyモデル(Taxonomyクラス)
(app/models/taxonomy.rb)
class Taxonomy < ApplicationRecord
  validates :name, presence: true, uniqueness: { scope: :type }, length: { maximum: 16 }
  validates :slug, presence: true, uniqueness: { scope: :type }, length: { maximum: 64 }, slug_format: true
end
Tagモデルなどサブクラス達を実装する。

Tag < TaxonomyみたいにTaxonomyクラスを継承している。

(app/models/tag.rb)
class Tag < Taxonomy
  has_many :article_tags
  has_many :articles, through: :article_tags
end
(app/models/category.rb)
class Category < Taxonomy
  has_many :articles
end
(app/models/author.rb)
class Author < Taxonomy
  has_many :articles
end

上記の実装をまとめると

  • Taxonomyクラスは共通の処理を纏めたスーパークラスで、TaxonomiesテーブルはDB上に実在するテーブルである。
  • Category、Tag、AuthorクラスはTaxonomyクラスを継承したサブクラスだが、categories、tags、authorsテーブルというものはDB上に存在しない
    しかし、STIを使用することで、開発者はCategories、Tags、Authorsテーブルというテーブルが存在するかのようにモデルとDBを扱えるようになるのです。

注意点

継承元のテーブルにはtypeカラムを実装しておく必要がある。
どういうことかと言うと、DB上にはTaxonomyテーブルしか存在しないので、category、tag、authorのデータはtaxonomiesテーブルの中に全部ごっちゃで入っていて、どのクラスのデータかを識別するためにtypeカラムを使うって感じ。
そこで、typeカラムというRailsが標準で用意しているカラムを使用すれば、ActiveRecordがいい感じにtypeにサブクラス名を入れてくれるのです。

上記の点から、普通のカラムとしてtypeカラムを使用するのは良くない。なぜならSTIの機能と判断してしまうから。タイプを表すカラムを使いたいならuser_typeみたいにする必要がある。

コンソールで遊んでみる

Taxonomyテーブルのレコードを全件取得すると、 Category、Tag、Authorクラスのデータが全て入っていて、typeカラムにクラス名が入っってることが分かる。

[4] pry(main)> Taxonomy.all
  Taxonomy Load (0.6ms)  SELECT `taxonomies`.* FROM `taxonomies`
=> [#<Author:0x00007f8 id: 1, type: "Author", name: "山田 太郎", slug: "tarou_yamada", description: "", created_at: Mon, 18 May 2020 12:29:46 JST +09:00, updated_at: Mon, 18 May 2020 12:29:46 JST +09:00>,
 #<Tag:0x00007f8 id: 2, type: "Tag", name: "日記", slug: "diary", description: nil, created_at: Mon, 18 May 2020 12:29:55 JST +09:00, updated_at: Mon, 18 May 2020 12:29:55 JST +09:00>,
 #<Category:0x00007f8 id: 4, type: "Category", name: "朝活", slug: "moring", description: nil, created_at: Mon, 18 May 2020 12:30:34 JST +09:00, updated_at: Mon, 18 May 2020 12:30:34 JST +09:00>,
 #<Author:0x00007f8 id: 5, type: "Author", name: "花子", slug: "hanako", description: "", created_at: Mon, 18 May 2020 13:57:41 JST +09:00, updated_at: Mon, 25 May 2020 17:03:22 JST +09:00>,

typeカラムがTagのレコードを全件取得。

[5] pry(main)> Taxonomy.where(type: "Tag")
  Taxonomy Load (0.5ms)  SELECT `taxonomies`.* FROM `taxonomies` WHERE `taxonomies`.`type` = 'Tag'
=> [#<Tag:0x00007f8fe20428a0 id: 2, type: "Tag", name: "日記", slug: "diary", description: nil, created_at: Mon, 18 May 2020 12:29:55 JST +09:00, updated_at: Mon, 18 May 2020 12:29:55 JST +09:00>,
 #<Tag:0x00007f8fe2042670 id: 3, type: "Tag", name: "エンジニア", slug: "engineer", description: nil, created_at: Mon, 18 May 2020 12:29:59 JST +09:00, updated_at: Mon, 18 May 2020 12:29:59 JST +09:00>,
 #<Tag:0x00007f8fe2042468 id: 6, type: "Tag", name: "毎日", slug: "every_day", description: nil, created_at: Mon, 18 May 2020 13:57:56 JST +09:00, updated_at: Mon, 18 May 2020 13:57:56 JST +09:00>]

Tagのデータを全件取得する。
ここで走っているSQL文はTaxonomyテーブルを対象にしていることが分かる。Tagsテーブルなんて存在しないからね。
でもSTIを使えば、Tagsテーブルが存在するかのように、ActiveRecordの検索メソッドを使えるのです。

[6] pry(main)> Tag.all
  Tag Load (0.5ms)  SELECT `taxonomies`.* FROM `taxonomies` WHERE `taxonomies`.`type` IN ('Tag')
=> [#<Tag:0x00007f8 id: 2, type: "Tag", name: "日記", slug: "diary", description: nil, created_at: Mon, 18 May 2020 12:29:55 JST +09:00, updated_at: Mon, 18 May 2020 12:29:55 JST +09:00>,
 #<Tag:0x00007f8 id: 3, type: "Tag", name: "エンジニア", slug: "engineer", description: nil, created_at: Mon, 18 May 2020 12:29:59 JST +09:00, updated_at: Mon, 18 May 2020 12:29:59 JST +09:00>,
 #<Tag:0x00007f8 id: 6, type: "Tag", name: "毎日", slug: "every_day", description: nil, created_at: Mon, 18 May 2020 13:57:56 JST +09:00, updated_at: Mon, 18 May 2020 13:57:56 JST +09:00>]

関連テーブルからタグの情報を簡単に取ってこれる。
find_by(type: "Tag")みたいにゴチャゴチャ書く必要がなく、特に意識せずサブクラスのデータを扱える。
SQLではWHERE 'taxonomies'.'type' IN ('Tag')みたいにTaxonomiesテーブルを、where句の中でtypeカラムを条件に絞り込む感じ)

[6] pry(main)> Article.first.tags
  Article Load (0.5ms)  SELECT  `articles`.* FROM `articles` ORDER BY `articles`.`id` ASC LIMIT 1
  Tag Load (0.7ms)  SELECT `taxonomies`.* FROM `taxonomies` INNER JOIN `article_tags` ON `taxonomies`.`id` = `article_tags`.`tag_id` WHERE `taxonomies`.`type` IN ('Tag') AND `article_tags`.`article_id` = 1
=> [#<Tag:0x00007f8fe0de99b0 id: 3, type: "Tag", name: "エンジニア", slug: "engineer", description: nil, created_at: Mon, 18 May 2020 12:29:59 JST +09:00, updated_at: Mon, 18 May 2020 12:29:59 JST +09:00>]

STIを実装したER図

最後に説明を図にまとめておく!

f:id:ryota21silva:20200609193101p:plain
STIの図

参照先

RESTful、リソース、URLとは

ソフトウェア開発においては、RESTの概念が重要

RESTfulとは

あらかじめ決められたリソースを定義し、それに沿ってアクションとhttpメソッドを関連づけるという考え方。統一された書き方でコードを記述できる。

  • URLを先に考えるのであって、ルーティングを先に考えるのでは無い。
    →URLが頭の中で先に決まってて、それをもとにルートを書いてあげるって感じ。
    →URL(=リソースを表している)は何を表示させるかを表しており、HTTPメソッドで何をしたいのかを決める。

リソースとは

「Web上に存在する、名前を持ったありとあらゆる情報のこと」(webを支える技術より)

一つ一つのリソースはURLによって一意の名前が付けられる(users/1みたいな)

RESTfulに考えるとは

  1. ルーティングの定義を先に考えるのではなく、どういうリソースを表示させたいかを考える
  2. URLパターン(ブラウザに表示されるURL)を考える
  3. ルーティングを定義する。

→URLとは、「リソースの目印」みたいな役割なので、URLからぱっと何をするのかを推測できないといけない。

URLは親子関係を保つべき。

お気に入りした掲示板一覧はboards/bookmarksみたいになるのが綺麗。(ただ、これは人によって意見が分かれるらしい)

つまり、7つのアクションをもとに作っていく原則があり、URLパターンを変えた方がいいとかの時は新しいアクションを作ったげる、てのが良さそう.

なぜ7つのアクション以外を定義するのか

例えば、掲示板削除のconfirmページ(確認ページ)を表示させたい場合に、わざわざconfirmコントローラを作るのはコストが高い。
→それなら、collectionルーティング で、boards#confirmアクションを増やしてあげればいいって感じ。
→ただ、用件が変わった時に修正するのが面倒というデメリットはある。

「リソース→URL→ルーティング」のRestfulな実装手順

要件:ブックマークした掲示板の一覧ページ(これがリソース)を表示させたい

例えばこんなルーティングを実装してみたとする。 これだと、 bookmarks/index.html.erbはブックマークのページを表示させるみたいに見える。

resources :bookmarks, only: %i[index]

→ bookmarks GET  /bookmarks(.:format)  bookmarks#index

今回取得したいリソースは 「ブックマークした掲示板一覧」だから、boards/bookmarksというURLの方が自然。
→「そのようなURLヘルパーを作るには?」と、ここでやっとルーティングの定義を考える。

resources :boards do
  collection do
    get :bookmarks
  end
end

bookmarks_boards GET    /boards/bookmarks(.:format)    boards#bookmarks

補足(memo)

boardsコントローラにbookmarksアクションを作る意味
  1. bookmarks/*以下のhtmlファイルは、ブックマークボタンの表示を切り替えるためのファイルとか、ajax送信行うためのjsファイルだけに纏められている。
  2. ブックマークした掲示板の表示とか、掲示板関連はboards/*以下のhtmlファイルに纏められた。
  3. それにURLも、URLヘルパーも直感的に分かりやすくなる。

あと、どんなリソースを表したいのか考えれば簡単。
ブックマークした掲示板一覧を表示させたいなら、URLにboardsbookmarksある方がどんなリソースを表しているのか直感的に分かりやすい。

memberとcollectionについて

用途
  • resourcesでroutingを設定しているとき、resourcesでは自動で生成されないactionへのroutingを設定するときに使用。
  • 生成するroutingに、:idが付くか付かないか。
違い
  • member…コレクションの各メンバーに対してアクションを追加します。idを伴う時に使う。
  • collection…コレクションそのものに対してアクションを追加します。idを伴わない時に使う。
resources :boards do
   resources :comments, only: %i[create], shallow: true
   collection do
     get :bookmarks
   end
end
GET      /boards/bookmarks(.:format)                  boards#bookmarks

Rspec備忘録(随時アップデート)

RSpecでテストを書く時に、よく使うけど忘れがちな文法とかをまとめていこうと思います。
※随時書き足していきたい

  • have_selector
have_selector 'h1', text: '大事なお知らせ'
expect(page).to have_selector 'a[data-method=delete]', text: 'delete'
expect(page).to have_selector 'h1', text: /^大事なお知らせ$/
  • 複数ある要素の内、最初の要素を選択
all('.box-footer')[0].click_on '更新する'
first('.box-footer').click_on '更新する'
  • 最後に開いたタブへの切り替え
switch_to_window(windows.last)
  • 画像を複数枚同時に保存できたことを検証する 複数枚画像を選択したいから、attach_fileで配列を使用。
    %Wは配列を作るパーセント記法で、式展開がされる。
context 'トップ画像を複数枚アップロード' do
  it 'トップ画像が複数枚保存される' do
    attach_file('site_main_images', %W(#{Rails.root}/spec/fixtures/images/eye_catch.jpg #{Rails.root}/spec/fixtures/images/alexandros.jpeg))
    click_on '保存'
    expect(page).to have_selector("img[src$='eye_catch.jpg']"), '複数画像が保存されていない'
    expect(page).to have_selector("img[src$='alexandros.jpeg']"), '複数画像が保存されていない'
  end
end
  • システムスペックで、ブラウザの挙動を確認 spec/rails_helper.rbpath/to/spec/support/**/*.rbを読み込めるように設定
(spec/support/web_driver.rb)
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by(:rack_test)
    driven_by(:selenium_chrome)
    # driven_by(:selenium_chrome_headless)
  end
end
(spec/rails_helper.rb)
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }

transactionについて

transactionとは

DBを使う場合、安全で確実なデータ操作とデータ管理が重要です。

例えば、お金の入出金で、出金者がちゃんとお金を振り込んだのに、入出金処理の途中で処理が中断され、入金の内容だけデータベースに反映されていない、なんてことが起こると大変。
そのために使われる仕組みの一つがトランザクション

コードで説明

以下の例だと、do~endのブロック内に記述している処理が、一つのトランザクションとみなされる。
この2つの処理のうち片方が失敗すれば、もう片方の処理をロールバックさせる。

つまり、トランザクション内の処理は「全部成功するか」「全部失敗するか」の2つしか無い。

また、例外処理が起きるとロールバックが発火するため、save!としている。

(app/controllers/admin/articles/article_blocks_controller.rb)
Article.transaction do
   @article.body = @article.build_body(self)
   @article.save!
end

追記

複数のデータベースをいじる時も、トランザクションでまとめると良い(業務アプリケーションでは必須)。

例: 
1. 親モデルをnew→親モデルをsaveして
2. 子モデル(関連テーブル)をbuild

ActiveRecord::Base.transactionで囲んで、save!とかで例外を起こすようにするだけ、

ActiveRecord::Base.transaction do
  DBをいじる処理など
  save!
end

local_assignメソッドって?

local_assignメソッドとは?

ローカル変数を特定の状況に限ってパーシャルに渡したい時に使う。

用途
  1. renderした部分テンプレート内で使える変数を明示したい(明示したいだけで、使わなくてもいけそう)
  2. 変数が見つからない時に|| 値とセットしたりできる。
    local_assigns[:width] || 853はwidthというローカル変数が見つからない時に、853という値を代入してくれる。
(app/views/admin/articles/article_blocks/_show_embed.html.slim)
.box-body
  - if embed.identifier?
    - if embed.youtube?
      = render 'shared/embed_youtube', embed: embed, width: 560, height: 315
    - if embed.twitter?
      = render 'shared/embed_twitter', embed: embed
(app/views/shared/_embed_youtube.html.slim)
ruby:
  embed = local_assigns[:embed]
  width = local_assigns[:width] || 853
  height = local_assigns[:height] || 480

RakeタスクとWheneverを使って記事ステータスを自動更新する

Rakeタスク

  library rake (Ruby 2.7.0 リファレンスマニュアル).
Rails で Rakeタスクを作る.
Rails の Rakefile がどのように lib/tasks の rake ファイルを読み込んでいるか.  

  • rbenvでRubyをインストールしていれば、Rakeも使える。
  • Rubyで作られた(記述された)ビルドツール。Rubyで記述できる。
  • 何かしらのまとまった処理(タスク)を簡単に実行するためのツールでもある。
  • 予め定義されているTaskは、必要であれば使うべし。
$ rake -T
rake about       # List versions of all Rails frameworks and the environment
rake active_storage:install # Copy over the migration needed to the application
rake status_update:article_status  # 記事が公開待ち状態で、公開日時が過去の場合、ステータスを「公開」にする
rake test        # Runs all tests in test folder except system ones

cron(クーロン)

多くのUNIX系OSで標準的に利用される常駐プログラム(デーモン)の一種で、利用者の設定したスケジュールに従って、指定されたプログラムを定期的に起動してくれるもの。
定期的にコマンドを実行するために、メモリ場で常に命令を待機しているプロセス(=デーモンプロセス)
。 cronを使えば、指定したプログラム=ジョブ(コマンドやシェルスクリプト)を一定の時間、日付、間隔で定期的に実行するようスケジュールできる。
「クーロンお前、いついつにコレやっとけよ!」って言っといたら、クーロン君は律儀に命令を守り、馬車馬の如く働いてくれるらしい。

crontabというコマンドでcronに指示をする。


whenever

Wheneverは導入が超簡単なcrontab管理ライブラリGemです!
Railsでwheneverを使ってcronを設定する
決まった時間に自動で処理してくれるwheneverの使い方

cronの仕組みをより扱いやすいように、Rubyを使って記述・拡張したツール。rubyを使って簡単にcronの設定を記述できるよってこと。

  • wheneverの設定はconfig/schedule.rbに書いてく。

require File.expand_path(File.dirname(__FILE__) + '/environment')Railsっていうプログラムを呼んでくる。
rails serverとかもプログラムを呼び出してる。
Cron、Rakeタスクとかは、Railsってプログラムとは別物だから明示的な記述が必要になる。

(config/schedule.rb)
# Rails.rootを使用するために必要
require File.expand_path(File.dirname(__FILE__) + '/environment')
bundle exec whenever --update-crontab
[write] crontab file updated
funesakisuke:/Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced [ryota21] $ crontab -l
# Begin Whenever generated tasks for: /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/config/schedule.rb at: 2020-05-14 16:09:28 +0900
0 * * * * /bin/bash -l -c 'cd /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced && RAILS_ENV=development bundle exec rake status_update:article_status --silent >> /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/log/cron.log 2>&1'
# End Whenever generated tasks for: /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/config/schedule.rb at: 2020-05-14 16:09:28 +0900

バッチ処理

バッチ処理について考える
コンピュータで1つの流れのプログラム群(ジョブ)を順次に実行すること。 あらかじめ定めた処理を一度に行うことを示すコンピュータ用語。


記事のステータスと公開日時の状態によって、ステータスを変更してくれるRakeタスクを作成。

(lib/tasks/status_update.rake)
# ディレクトリとnamespaceは合わせる
namespace :status_update do
  desc '記事が公開待ち状態で、公開日時が過去の場合、ステータスを「公開」にする'
  # environmentでDBに接続
  task article_status: :environment do
    @articles = Article.where(state: :publish_wait)
    @articles.each do |article|
      if article.published_at.past?
        article.state = :published
        article.save!
      end
    end
  end
end

上記は以下のように記述できる。(&:メソッド)でprocを使える。

(lib/tasks/status_update.rake)
namespace :status_update do
  desc '記事が公開待ち状態で、公開日時が過去の場合、ステータスを「公開」にする'
  task article_status: :environment do
    @articles = Article.publish_wait.past_publish.find_each(&:published!)
  end
end

cronの実行対象であるRakeタスクのテスト

Rakeタスクのテストの仕方

(spec/lib/tasks/status_update_spec.rb)
require 'rails_helper'
require 'rake'
describe 'StatusUpdate' do
  # すべてのテストの前に1度だけ実行
  before(:all) do
    # Rake::Applicationクラスは何...?
    @rake = Rake::Application.new
    # Rakeは、Rake というコマンドラインツールを扱うライブラリ
    Rake.application = @rake
    # 指定のRakeファイルを読み込む
    Rake.application.rake_require 'tasks/status_update'
    # Rakeタスクを実行する環境?
    Rake::Task.define_task(:environment)
  end
  # eachで各テストの前に毎回実行
  before(:each) do
    # Rakeタスクを使ったテストを何回か実行する時、2回目以降もRakeタスクを実行してくれる?
    @rake[task_name].reenable
  end
  describe '記事ステータスを更新するTask' do
    # task_nameの定義必要?
    let!(:task_name) { 'status_update:article_status' }
    let!(:publish_wait) { create(:article, :publish_wait, published_at: "2019-12-05 12:00" ) }
    let!(:user) { create(:user, :admin) }
    context 'ステータスが「公開待ち」で、公開日時が過去になっている場合' do
      it 'ステータスを「公開」に更新すること' do
        # invokeでRakeタスクを呼び出す
        @rake[task_name].invoke
        expect(publish_wait.state).to eq "published"
      end
    end
  end
end

上記テストのデバッグをしてみる

$ bundle exec rspec spec/lib/tasks/status_update_spec.rb
== Seed from /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/db/fixtures/test/sites.rb
 - Site {:id=>1, :name=>"Blog", :subtitle=>"Very awesome!"}
StatusUpdate
  記事ステータスを更新するTask
    ステータスが「公開待ち」で、公開日時が過去になっている場合
From: /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/lib/tasks/status_update.rake @ line 11 :
     6:     @articles.each do |article|
     7:       if article.published_at.past?
     8:         
     9:         binding.pry
    10:         
 => 11:         article.state = :published
    12:         article.save!
    13:       end
    14:     end
    15:   end
    16: end
[1] pry(main)> article.state
=> "publish_wait"
[2] pry(main)> next
From: /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/lib/tasks/status_update.rake @ line 12 :
     7:       if article.published_at.past?
     8:         
     9:         binding.pry
    10:         
    11:         article.state = :published
 => 12:         article.save!
    13:       end
    14:     end
    15:   end
    16: end
[2] pry(main)> article.state
=> "published"
[3] pry(main)> next
From: /Users/funesakisuke/.rbenv/versions/2.6.5/lib/ruby/2.6.0/monitor.rb @ line 237 MonitorMixin#mon_synchronize:
    230: def mon_synchronize
    231:   # Prevent interrupt on handling interrupts; for example timeout errors
    232:   # it may break locking state.
    233:   Thread.handle_interrupt(EXCEPTION_NEVER){ mon_enter }
    234:   begin
    235:     yield
    236:   ensure
 => 237:     Thread.handle_interrupt(EXCEPTION_NEVER){ mon_exit }
    238:   end
    239: end
[3] pry(#<Monitor>)> next
From: /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/spec/lib/tasks/status_update_spec.rb @ line 34 :
    29:     context 'ステータスが「公開待ち」で、公開日時が過去になっている場合' do
    30:       it 'ステータスを「公開」に更新すること' do
    31:         # invokeでRakeを呼び出す?
    32:         @rake[task_name].invoke
    33:         
 => 34:         binding.pry
    35:         
    36:         expect(publish_wait.reload.state).to eq "published"
    37:       end
    38:     end
    39:   end
[3] pry(#<RSpec::ExampleGroups::StatusUpdate::Task::Nested>)> publish_wait.state
=> "publish_wait"
[4] pry(#<RSpec::ExampleGroups::StatusUpdate::Task::Nested>)> @rake[task_name] 
=> <Rake::Task status_update:article_status => [environment]>
[5] pry(#<RSpec::ExampleGroups::StatusUpdate::Task::Nested>)> next
From: /Users/funesakisuke/workspace/runteq/332_ryota1116_runteq_learning_advanced/spec/lib/tasks/status_update_spec.rb @ line 36 :
    31:         # invokeでRakeを呼び出す?
    32:         @rake[task_name].invoke
    33:         
    34:         binding.pry
    35:         
 => 36:         expect(publish_wait.reload.state).to eq "published"
    37:       end
    38:     end
    39:   end
    40: 
    41: end
[5] pry(#<RSpec::ExampleGroups::StatusUpdate::Task::Nested>)> publish_wait.reload.state
=> "published"
[6] pry(#<RSpec::ExampleGroups::StatusUpdate::Task::Nested>)> publish_wait.state
=> "published"