Vue.jsで上に戻るボタンを実装する
はじめに
Vue.jsで下にスクロールした際に、上に戻れるボタンを実装する方法について。
上に戻るボタンを追加
まずはreturnTop()
というクリックイベントを設定します。
<div class="return_top_button btn btn-secondary" @click="returnTop()"> <font-awesome-icon icon="chevron-circle-up" /> </div>
続いてreturnTop()
メソッドの処理を追加します。
<script> export default { name: 'JokeIndex', methods: { returnTop() { window.scrollTo({ top: 0, behavior: 'smooth' }) } </script> <style scoped> .return_top_button { position: fixed; right: 15px; bottom: 15px; } </style>
window.scrollTo
メソッドを使って指定の位置までスクロールするように設定します。
behavior: 'smooth'
は、ぬるっとスクロールする指定です。
デフォルトだと、一瞬でスクロールされます。
下にスクロールしたときにボタンを表示させる
続いて、下にスクロールしたときだけ、上に戻るボタンを表示させるように設定します。
<transition name="return_button"> // トランジション設定 <div class="return_top_button btn btn-secondary" @click="returnTop()" v-show="returnTopActive"> <font-awesome-icon icon="chevron-circle-up" /> </div> </transition>
v-show
を使って、returnTopActive
がtrueのときのみ表示されるようにします。
<script> export default { name: 'JokeIndex', data() { return { returnTopActive: false, scroll: 0, } }, mounted() { window.addEventListener('scroll', this.scrollWindow) // スクロールの際に発火 }, methods: { returnTop() { window.scrollTo({ top: 0, behavior: 'smooth' }), scrollWindow() { this.scroll = window.scrollY // 縦方向のスクロール量を取得 if ( 100 <= this.scroll ) { this.returnTopActive = true } else { this.returnTopActive = false } }, } </script> <style scoped> .return_top_button { position: fixed; right: 15px; bottom: 15px; } // enter/leaveトランジションで滑らかに表示・非表示させる .return_button-enter-active, .return_button-leave-active { transition: opacity .5s; } .return_button-enter, .return_button-leave-to { opacity: 0; } </style>
完成
yarnを使ってVue.jsにFontAwesomeを導入する
はじめに
yarnを使ってVue.jsにFontAwesomeを導入する方法について
導入方法
まずはyarnを使ってFontAwesomeをインストールします。
$ yarn add @fortawesome/fontawesome-svg-core $ yarn add @fortawesome/free-solid-svg-icons $ yarn add @fortawesome/vue-fontawesome
続いて、hello_vue.js
ファイルにインストールしたものをインポートします。
import Vue from 'vue' import App from '../app.vue' import router from '../router' import axios from '../plugins/axios' import store from '../store' import 'bootstrap/dist/css/bootstrap.css' import '../plugins/veevalidate' // ここから import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronCircleUp } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' library.add(faChevronCircleUp) Vue.component('font-awesome-icon', FontAwesomeIcon) // ここまで Vue.config.productionTip = false; Vue.prototype.$axios = axios document.addEventListener('DOMContentLoaded', () => { const app = new Vue({ router, store, render: h => h(App) }).$mount() document.body.appendChild(app.$el) })
import { faChevronCircleUp } from '@fortawesome/free-solid-svg-icons' library.add(faChevronCircleUp)
上記の2つの部分には使いたいアイコンを指定します。
複数アイコンを使う場合はカンマ区切りでインポートします。
一括でアイコンをインポートする場合は下記のようにします。
import { fas } from '@fortawesome/free-solid-svg-icons' library.add(fas)
最後に、該当のページで先程インポートしたアイコンを表示します。
<div class="return_top_button btn btn-secondary" v-show="returnTopActive" @click="returnTop()"> <font-awesome-icon icon="chevron-circle-up" /> // この部分 </div>
GitHub
Vue.jsでリアルタイム検索機能をつくる
はじめに
Vue.jsでリアルタイム検索機能をつくる方法について
設定方法
<template> <div class="container-fluid"> <div id="search-form" class="form-row p-3" > <div class="form-group col-lg-6 offset-lg-3"> <label for="search">絞り込み</label> <input id="search" v-model="keyword" // 入力された値を取得する type="text" placeholder="タスク名を入力してください" class="form-control" > </div> </div> ............ ............ ............ </div> </template> <script> import { mapGetters, mapActions } from 'vuex' import TaskDetailModal from './components/TaskDetailModal' import TaskCreateModal from './components/TaskCreateModal' import TaskEditModal from './components/TaskEditModal' import TaskList from './components/TaskList' export default { name: "TaskIndex", components: { TaskDetailModal, TaskCreateModal, TaskEditModal, TaskList }, data() { return { taskDetail: {}, isVisibleTaskDetailModal: false, isVisibleTaskCreateModal: false, isVisibleTaskEditModal: false, taskEdit: {}, keyword: '', // 初期値を設定 } }, computed: { ...mapGetters('tasks', [ 'tasks' ]), ...mapGetters("users", ["authUser"]), todoTasks() { return this.filteredTasks.filter(task => { return task.status == "todo" }) }, doingTasks() { return this.filteredTasks.filter(task => { return task.status == "doing" }) }, doneTasks() { return this.filteredTasks.filter(task => { return task.status == "done" }) }, // 検索欄に文字が入力されるたびにこの算出プロパティが実行される filteredTasks() { return this.tasks.filter(task => { return task.title.indexOf(this.keyword) != -1 // indexOfは該当しない場合は-1を返す }) } }, ........... ........... ........... } </script>
VeeValidateを使ってバリデーションをつくる
はじめに
VeeValidateを使ってバリデーションをつくる方法について
VeeValidateをインストール
$ yarn add vee-validate
VeeValidateの設定ファイルを作成
$ touch app/javascript/plugins/veevalidate.js
上記のファイルをインクルードします。
javascript/packs/hello_vue.js
... ... import '../plugins/veevalidate' ... ...
VeeValidateの設定
先程作成したplugins/veevalidate.js
ファイルに設定を書きます
javascript/plugins/veevalidate.js
import Vue from 'vue' import { extend, ValidationProvider, ValidationObserver } from 'vee-validate'; // 使用するバリデーションのルールをインポート import { required, email, confirmed, min } from 'vee-validate/dist/rules'; // インポートしたルールのエラーメッセージを日本語にする extend('required', { ...required, message: "{_field_}は必須項目です" }); extend('email', { ...email, message: "{_field_}の形式で入力してください" }); extend('confirmed', { ...confirmed, message: "パスワードと一致しません" }); extend('min', { ...min, params: ['length'], message: "{_field_}は{length}文字以上で入力してください" }); Vue.component('ValidationProvider', ValidationProvider); Vue.component('ValidationObserver', ValidationObserver);
バリデーションをフォームに設定
ユーザー登録フォーム
<template> <div id="register-form" class="container w-50 text-center" > <div class="h3 mb-3"> ユーザー登録 </div> // フォーム全体をvalidation-observeで囲む // handleSubmitを使ってフォーム送信前にバリデーションを自動で実行する <validation-observer v-slot="{ handleSubmit }"> <div class="form-group text-left"> <label for="name">ユーザー名</label> // 各フォームをvalidation-providerで囲む <validation-provider v-slot="{ errors }" // エラーメッセージを格納する name="ユーザー名" rules="required" // 使用するバリデーションルールを指定する > <input id="name" v-model="user.name" type="text" class="form-control" placeholder="username" > <p class="text-danger"> {{ errors[0] }} // 格納したエラーメッセージを表示する </p> </validation-provider> </div> <div class="form-group text-left"> <label for="email">メールアドレス</label> <validation-provider v-slot="{ errors }" name="メールアドレス" rules="required|email" > <input id="email" v-model="user.email" type="email" class="form-control" placeholder="test@example.com" > <p class="text-danger"> {{ errors[0] }} </p> </validation-provider> </div> <div class="form-group text-left"> <label for="password">パスワード</label> <validation-provider v-slot="{ errors }" name="パスワード" rules="required" vid="password" // vidを指定 > <input id="password" v-model="user.password" type="password" class="form-control" placeholder="password" > <p class="text-danger"> {{ errors[0] }} </p> </validation-provider> </div> <div class="form-group text-left"> <label for="password_confirmation">パスワード(確認)</label> <validation-provider v-slot="{ errors }" name="パスワード(確認)" rules="required|min:3|confirmed:password" // vidで指定したフォームの入力内容を取得 > <input id="password_confirmation" v-model="user.password_confirmation" type="password" class="form-control" placeholder="password" > <p class="text-danger"> {{ errors[0] }} </p> </validation-provider> </div> <button type="submit" class="btn btn-primary" @click="handleSubmit(register)" // handleSubmitでフォーム送信前にバリデーションをチェック > 登録 </button> </validation-observer> </div> </template> <script> export default { name: "RegisterIndex", data() { return { user: { name: '', email: '', password: '', password_confirmation: '', } } }, methods: { register() { this.$axios.post('users', { user: this.user }) .then(res => { this.$router.push({ name: 'LoginIndex' }) }) .catch(err => { console.log(err) }) } } } </script> <style scoped> </style>
JWTを使ってトークンベースの認証機能をつくる
はじめに
JWT
を使ってトークンベースの認証機能をつくる方法について
JWTをインストール
gem 'jwt'
bundle install
sessionsコントローラの設定
controllers/api/sessions_controller.rb
class Api::SessionsController < ApplicationController def create user = User.authenticate(params[:email], params[:password]) # sorceryのメソッド if user token = user.create_tokens # トークンを作成 render json: { token: token } # トークンを返す else head :unauthorized end end end
トークンを作成する処理を設定
トークンを作成する処理をモジュールに切り出します。
models/concerns/jwt_token.rb
module JwtToken extend ActiveSupport::Concern class_methods do def decode(token) JWT.decode token, Rails.application.secrets.secret_key_base # デコードする end end def create_tokens payload = { user_id: id } issue_token(payload.merge(exp: Time.current.to_i + 1.month)) # 期間をマージ end private def issue_token(payload) JWT.encode payload, Rails.application.secrets.secret_key_base # エンコードする end end
userモデルにモジュールをインクルードします。
class User < ApplicationRecord include JwtToken . . end
ログインページを作成
javascript/pages/login/index.vue
<template> <div id="login-form" class="container w-50 text-center" > <div class="h3 mb-3"> ログイン </div> <validation-observer v-slot="{ handleSubmit }"> <div class="form-group text-left"> <label for="email">メールアドレス</label> <validation-provider v-slot="ProviderProps" name="メールアドレス" rules="required|email" > <input id="email" v-model="user.email" type="email" class="form-control" placeholder="test@example.com" > <p class="error text-danger"> {{ ProviderProps.errors[0] }} </p> </validation-provider> </div> <div class="form-group text-left"> <label for="password">パスワード</label> <validation-provider v-slot="ProviderProps" name="パスワード" rules="required|min:3" > <input id="password" v-model="user.password" type="password" class="form-control" placeholder="password" > <p class="error text-danger"> {{ ProviderProps.errors[0] }} </p> </validation-provider> </div> <button type="submit" class="btn btn-primary" @click="handleSubmit(login)" > ログイン </button> </validation-observer> </div> </template> <script> import { mapActions } from "vuex" export default { name: "LoginIndex", data() { return { user: { email: "", password: "", } } }, methods: { ...mapActions("users", [ "loginUser", // storeで定義するactions ]), async login() { try { await this.loginUser(this.user); this.$router.push({ name: 'TaskIndex' }) } catch (error) { console.log(error); } } } } </script> <style scoped> </style>
上記で使用するloginUser
をstoreで定義する
javascript/plugins/axios.js
import axios from '../../plugins/axios' const state = { ... } const getters = { ... } const mutations = { ... } const actions = { async loginUser({ commit }, user) { const sessionsResponse = await axios.post('sessions', user) // /sessionsにpostリクエスト // 返ってきたトークンをローカルストレージに保存 localStorage.auth_token = sessionsResponse.data.token // リクエストの認証ヘッダーにもトークンを保存 axios.defaults.headers.common['Authorization'] = `Bearer ${localStorage.auth_token}` }, } export default { namespaced: true, state, getters, mutations, actions }
axiosのデフォルトヘッダーにトークンを追加
javascript/plugins/axios.js
import axios from 'axios' const axiosInstance = axios.create({ baseURL: 'api' }) // デフォルトヘッダーにいれることでapi通信の際にトークンを毎回設定しなくて済む if (localStorage.auth_token) { axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${localStorage.auth_token}` } export default axiosInstance
トークンで認証を行う
controllers/concerns/api/user_authenticator.rb
module Api::UserAuthenticator extend ActiveSupport::Concern # トークンからユーザーを割り出す def current_user return @current_user if @current_user return unless bearer_token # トークンをデコードしてuser_idを取得する payload, = User.decode bearer_token @current_user ||= User.find_by(id: payload['user_id']) rescue JWT::ExpiredSignature nil end def authenticate! return if current_user head :unauthorized end # Bearerトークンを取得する def bearer_token pattern = /^Bearer / header = request.headers['Authorization'] header.gsub(pattern, '') if header&.match(pattern) end end
上記のモジュールをインクルードする
controllers/application_controller.rb
class ApplicationController < ActionController::Base include Api::UserAuthenticator protect_from_forgery with: :null_session end
current_userを返すapiを定義する
class Api::UsersController < ApplicationController ... ... def me render json: current_user end private ... ...
Rails.application.routes.draw do root to: 'home#index' namespace :api, format: 'json' do resources :tasks resources :sessions resources :users do collection do get 'me' end end end get '*path', to: 'home#index' end
ヘッダーを修正する
ログイン前と後でヘッダーの表示項目を変更する。
javascript/components/TheHeader.vue
<template> <header> <nav class="navbar navbar-expand navbar-dark bg-dark justify-content-between"> <span class="navbar-brand mb-0 h1">タスク管理アプリ</span> <ul class="navbar-nav"> <template v-if="!authUser"> // ログインしてるかどうか <li class="nav-item active"> <router-link :to="{ name: 'RegisterIndex' }" class="nav-link" > ユーザー登録 </router-link> </li> <li class="nav-item active"> <router-link :to="{ name: 'LoginIndex' }" class="nav-link" > ログイン </router-link> </li> </template> <template v-else> <li class="nav-item active avatar-image-wrapper"> <img :src="authUser.avatar_url" class="rounded avatar-image" > </li> <li class="nav-item active"> <router-link :to="{ name: 'ProfileIndex' }" class="nav-link" > プロフィール </router-link> </li> <li class="nav-item active"> <router-link to="#" class="nav-link" @click.native="handleLogout" > ログアウト </router-link> </li> </template> </ul> </nav> </header> </template> <script> import { mapGetters, mapActions } from "vuex" export default { name: "TheHeader", computed: { ...mapGetters("users", ["authUser"]) }, methods: { ...mapActions("users", ["logoutUser"]), // ログアウト async handleLogout() { try { await this.logoutUser() this.$router.push({name: 'TopIndex'}) } catch (error) { console.log(error) } } } } </script> <style scoped> .avatar-image-wrapper { line-height: 40px; } .avatar-image { width: 20px; } </style>
storeでユーザーのログイン、ログアウトの処理を作成します。
javascript/store/modules/users.js
import axios from '../../plugins/axios' const state = { authUser: null } const getters = { authUser: state => state.authUser } const mutations = { setUser: (state, user) => { state.authUser = user } } const actions = { async loginUser({ commit }, user) { // ログイン const sessionsResponse = await axios.post('sessions', user) localStorage.auth_token = sessionsResponse.data.token axios.defaults.headers.common['Authorization'] = `Bearer ${localStorage.auth_token}` // ログインユーザー情報の取得 const userResponse = await axios.get('users/me') commit('setUser', userResponse.data) }, logoutUser({ commit }) { // ログアウト、ローカルストレージとデフォルトヘッダーからトークンを削除 localStorage.removeItem('auth_token') axios.defaults.headers.common['Authorization'] = '' commit('setUser', null) }, } export default { namespaced: true, state, getters, mutations, actions }
タスクの編集・削除の権限を変更
自分の作成したタスクのみ編集・削除できるように修正します。
javascript/pages/task/index.vue
<template> <div class="container-fluid"> <div id="search-form" class="form-row p-3" > .......... .......... .......... <transition name="fade"> <TaskDetailModal v-if="isVisibleTaskDetailModal" :task="taskDetail" :auth-user="authUser" // 追加 @close-modal="handleCloseTaskDetailModal" @show-edit-modal="handleShowTaskEditModal" @delete-task="handleDeleteTask" /> </transition> ........... ........... </div> </template> <script> import { mapGetters, mapActions } from 'vuex' import TaskDetailModal from './components/TaskDetailModal' import TaskCreateModal from './components/TaskCreateModal' import TaskEditModal from './components/TaskEditModal' import TaskList from './components/TaskList' export default { name: "TaskIndex", components: { TaskDetailModal, TaskCreateModal, TaskEditModal, TaskList }, data() { return { taskDetail: {}, isVisibleTaskDetailModal: false, isVisibleTaskCreateModal: false, isVisibleTaskEditModal: false, taskEdit: {}, } }, computed: { ...mapGetters('tasks', [ 'tasks' ]), ...mapGetters("users", ["authUser"]), // 追加 }, ...... ...... ...... } </script>
javascript/pages/task/components/TaskDetailModal.vue
<template> <div :id="'task-detail-modal-' + task.id"> <div class="modal" @click.self="handleCloseModal" > <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title"> {{ task.title }} </h5> <button type="button" class="close" @click="handleCloseModal" > <span>×</span> </button> </div> <div v-if="task.description" class="modal-body" > <p>{{ task.description }}</p> </div> <div class="modal-footer"> <template v-if="isAuthUserTask"> // 追加 <button type="button" class="btn btn-success" @click="handleShowTaskEditModal" > 編集 </button> <button type="button" class="btn btn-danger" @click="handleDeleteTask" > 削除 </button> </template> <button type="button" class="btn btn-secondary" @click="handleCloseModal" > 閉じる </button> </div> </div> </div> </div> <div class="modal-backdrop show" /> </div> </template> <script> export default { name: "TaskDetailModal", props: { task: { type: Object, required: true, id: { type: Number, required: true }, title: { type: String, required: true }, description: { type: String, required: true }, status: { type: String }, user_id: { type: Number, required: true }, }, // authUserを受け取る authUser: { type: Object, required: true, id: { type: Number, required: true } } }, computed: { // 自分のタスクかどうかを判定 isAuthUserTask() { return this.task.user_id === this.authUser.id } }, methods: { handleCloseModal() { this.$emit('close-modal') }, handleShowTaskEditModal() { this.$emit('show-edit-modal', this.task) }, handleDeleteTask() { this.$emit('delete-task', this.task) } } } </script> <style scoped> .modal { display: block; } </style>
ログイン状態の保持を設定
javascript/router/index.vue
import Vue from "vue"; import Router from "vue-router"; import store from '../store'; import TopIndex from "../pages/top/index"; import TaskIndex from "../pages/task/index"; import RegisterIndex from "../pages/register/index"; import LoginIndex from "../pages/login/index"; import ProfileIndex from "../pages/profile/index"; Vue.use(Router) const router = new Router({ mode: "history", routes: [ { path: "/", component: TopIndex, name: "TopIndex", }, { path: "/tasks", component: TaskIndex, name: "TaskIndex", meta: { requiredAuth: true }, // ログイン済みのときのみアクセス可能 }, { path: "/register", component: RegisterIndex, name: "RegisterIndex", }, { path: "/login", component: LoginIndex, name: "LoginIndex", }, ], }) // router.beforeEachを使ってページ遷移時、リロード時に毎回この処理を実行する // storeのfetchAuthUserを実行してログイン状態を保持する // ログインユーザーがいないかつログイン必須のページに遷移する場合はログインページに飛ばす。 router.beforeEach((to, from, next) => { store.dispatch('users/fetchAuthUser').then((authUser) => { if (to.matched.some(record => record.meta.requiredAuth) && !authUser) { next({ name: 'LoginIndex' }); } else { next(); } }) }); export default router
storeにfetchAuthUser
を定義
javascript/store/modules/users.rb
import axios from '../../plugins/axios' const state = { authUser: null } const getters = { authUser: state => state.authUser } const mutations = { setUser: (state, user) => { state.authUser = user } } const actions = { async loginUser({ commit }, user) { // ログイン const sessionsResponse = await axios.post('sessions', user) localStorage.auth_token = sessionsResponse.data.token axios.defaults.headers.common['Authorization'] = `Bearer ${localStorage.auth_token}` // ログインユーザー情報の取得 const userResponse = await axios.get('users/me') commit('setUser', userResponse.data) }, logoutUser({ commit }) { // ログアウト localStorage.removeItem('auth_token') axios.defaults.headers.common['Authorization'] = '' commit('setUser', null) }, // ログイン状態の保持 async fetchAuthUser({ commit, state }) { if (!localStorage.auth_token) return null if (state.authUser) return state.authUser const userResponse = await axios.get('users/me') .catch((err) => { return null }) if (!userResponse) return null const authUser = userResponse.data if (authUser) { commit('setUser', authUser) return authUser } else { commit('setUser', null) return null } }, updateUser({ commit, state }, user) { return axios.patch(`profile/${state.authUser.id}`, user) .then(res => { commit('setUser', res.data) }) } } export default { namespaced: true, state, getters, mutations, actions }
railsアプリにCircleCIを導入する
はじめに
railsアプリにCircleCIを導入してCI部分のRspecとRubocopを自動化してみました。
設定方法
まずはアプリのルートディレクトリに.circleci
というディレクトリを作り、その配下にconfig.yml
というファイルを作ります。
そのファイルにcircleciの設定を記述していきます。
version: 2 jobs: build: # workflowsを使わない場合はbuildというジョブを定義する docker: - image: circleci/ruby:2.7.2-node-browsers-legacy # 使用するdockerのイメージを指定する environment: - BUNDLER_VERSION: 2.1.4 # bundlerのバージョン指定 - RAILS_ENV: test # 実行するデータベース名を指定 # database.yml.ciで使用する環境変数を設定 - MYSQL_HOST: 127.0.0.1 - MYSQL_USER: root - MYSQL_PASSWORD: '' - MYSQL_PORT: 3306 - TZ: "Japan" # データベース側の環境 - image: circleci/mysql:5.7 environment: - MYSQL_ALLOW_EMPTY_PASSWORD: 'true' - MYSQL_ROOT_HOST: '%' working_directory: ~/repo steps: # githubのリポジトリを取得 - checkout # yarnインストール - run: name: yarn Install command: yarn install # キャッシュがあればリストアする - restore_cache: keys: - v1-dependencies-{{ checksum "Gemfile.lock" }} - v1-dependencies- # bundlerのインストールとbundle install実行 - run: name: Install dependencies command: | gem install bundler -v 2.1.4 # 並列処理 bundle install --path=vendor/bundle --jobs 4 --retry 3 # キャッシュする - save_cache: key: v1-dependencies-{{ checksum "Gemfile.lock" }} paths: - vendor/bundle # データベースのセットアップ - run: name: Database Setup command: | mv ./config/database.yml.ci ./config/database.yml bundle exec rake db:create bundle exec rake db:schema:load # rubocop実行 - run: name: Rubocop command: bundle exec rubocop # rspec実行 - run: name: Run rspec command: | mkdir ./tmp/test-results # テスト結果を保存するディレクトリを作成 # テストを分割して実行する設定 TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \ circleci tests split --split-by=timings)" bundle exec rspec \ --format progress \ --format RspecJunitFormatter \ --out /tmp/test-results/rspec.xml \ --format progress \ $TEST_FILES # 結果を保存 - store_test_results: path: /tmp/test-results # テスト結果をCircleCIアプリのARTIFACTSに表示する - store_artifacts: path: /tmp/test-results destination: test-results
test: &default adapter: mysql2 encoding: utf8 charset: utf8 pool: 5 username: <%= ENV.fetch("MYSQL_USER") %> password: <%= ENV.fetch("MYSQL_PASSWORD") %> host: <%= ENV.fetch("MYSQL_HOST") %> port: <%= ENV.fetch("MYSQL_PORT") %> database: ci_test
ローカルで動作確認
$ brew install circleci
下記コマンドで.circleci.config.yml
ファイルの文法をチェック
$ circleci config validate .circleci/config.yml
下記コマンドで指定したジョブを実行
$ circleci local execute --job build
【エラー】Mysql2::Error: Incorrect string valueの対処法
はじめに
Mysql2::Error: Incorrect string value
の対処法について
該当のエラー
ActiveRecord::StatementInvalid: Mysql2::Error: Incorrect string value: ....... ..............
原因
character_set_database
とcharacter_set_server
の値がlatin1
になっていることが原因でした。
mysql> show variables like "chara%"; +--------------------------+-------------------------------------------+ | Variable_name | Value | +--------------------------+-------------------------------------------+ | character_set_client | utf8 | | character_set_connection | utf8 | | character_set_database | latin1 | | character_set_filesystem | binary | | character_set_results | utf8 | | character_set_server | latin1 | | character_set_system | utf8 | | character_sets_dir | /rdsdbbin/mysql-5.7.34.R5/share/charsets/ | +--------------------------+-------------------------------------------+ 8 rows in set (0.01 sec)
この値がlatin1
だと日本語ではなくアルファベットで保存されるそう。
なのでこの値をutf8
に変更しました。
AWSのRDSを使っていたので、該当のデータベースに関連付けされているパラメータグループから変更しました。