RubyのRefinements拡張
Rubyはモンキーパッチが簡単にできてしまうが、多用するとコードの複雑性が増してトレースが困難になるため、基本的に世の中一般のプロジェクトでは禁止されている(はず)。
それでも実装を進めていて拡張したほうが汎用性が増すケースもある。その場合はよく Refinements
を使用している。
配列内の数値について平均を求める場合、以下のようになるが、
array = [1, 2, 3, 4] array.sum / array.length.to_f
この処理をメソッドとして分離したとしても、引数として配列を渡すのがどうにもしっくりこないと感じているため、
refine Array
を行うmoduleを作成して、定義が必要なクラスでのみ呼び出すようにしている。
def average(array) array.sum / array.length.to_f end
module ArrayEx refine Array do def average sum / length.to_f end end end using ArrayEx [1, 2, 3, 4].average
Rails 多対多テーブル構造をcollection_check_boxesで関連付ける
formを用いた際の多対多テーブルの関連付けを、collection_check_boxesを使用して行う。
テーブル構造
create_table "users", force: :cascade do |t| t.string "name" t.datetime "created_at" t.datetime "updated_at" end create_table "services", force: :cascade do |t| t.string "name" t.datetime "created_at" t.datetime "updated_at" end create_table "users_services", force: :cascade do |t| t.integer "user_id" t.integer "service_id" t.datetime "created_at" t.datetime "updated_at" end
モデル
class User < ApplicationRecord has_many :users_services has_many :services, through: :users_services end
class Service < ApplicationRecord has_many :users_services has_many :users, through: :users_services end
class UsersService < ApplicationRecord belongs_to :user belongs_to :service end
コントローラー
class UsersController < ApplicationController def new @user = User.new @services = Service.all end def create user = User.new(permit_params) user.save! end private def permit_params params.require(:user).permit(:name, service_ids: []) end end
User
モデルの service_ids
メソッドに対して値を送信できるようにビューの定義を行う。
ビュー
※slim構文にて定義
= form_with scope: :user, mdoel: @user, url: users_path do |form| = form.text_field :name = form.collection_check_boxes :service_ids, @services, :id, :name, include_hidden: false = form.submit 'post'
collection_check_boxes
が services
テーブルに登録したレコードのID・名称を展開して表示を行う。
include_hidden
は空白文字をhiddenで定義するかオプションで設定を変更できる(不要なので今回はfalse)。
任意の内容で入力を行いsubmitすると、関連も含めてレコードを保存できる。
web_1 | Started POST "/users" for 172.18.0.1 at 2023-05-03 07:46:45 +0000 web_1 | Cannot render console from 172.18.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1 web_1 | Processing by UsersController#create as TURBO_STREAM web_1 | Parameters: {"authenticity_token"=>"[FILTERED]", "user"=>{"name"=>"test", "service_ids"=>["1", "2"]}, "commit"=>"post"} web_1 | Service Load (0.3ms) SELECT `services`.* FROM `services` WHERE `services`.`id` IN (1, 2) web_1 | ↳ app/controllers/users_controller.rb:8:in `create' web_1 | TRANSACTION (0.2ms) BEGIN web_1 | ↳ app/controllers/users_controller.rb:9:in `create' web_1 | User Create (0.7ms) INSERT INTO `users` (`name`, `created_at`, `updated_at`) VALUES ('test', '2023-05-03 07:46:45.726583', '2023-05-03 07:46:45.726583') web_1 | ↳ app/controllers/users_controller.rb:9:in `create' web_1 | UsersService Create (0.6ms) INSERT INTO `users_services` (`user_id`, `service_id`, `created_at`, `updated_at`) VALUES (1, 1, '2023-05-03 07:46:45.730791', '2023-05-03 07:46:45.730791') web_1 | ↳ app/controllers/users_controller.rb:9:in `create' web_1 | UsersService Create (0.2ms) INSERT INTO `users_services` (`user_id`, `service_id`, `created_at`, `updated_at`) VALUES (1, 2, '2023-05-03 07:46:45.732966', '2023-05-03 07:46:45.732966') web_1 | ↳ app/controllers/users_controller.rb:9:in `create' web_1 | TRANSACTION (7.5ms) COMMIT web_1 | ↳ app/controllers/users_controller.rb:9:in `create' web_1 | No template found for UsersController#create, rendering head :no_content web_1 | Completed 204 No Content in 60ms (ActiveRecord: 10.1ms | Allocations: 11039)
Ruby3.2 + Rails7の環境構築(docker compose)
クィックスタート: Compose と Rails — Docker-docs-ja 20.10 ドキュメント
こちらのドキュメントが少し古かったので、他の記事を見ながら個人的に構築した際のメモ。
作業ディレクトリの作成
mkdir myapp
以下のファイルを作成
Dockerfile
docker-compose.yml
Gemfile
Gemfile.lock
entrypoint.sh
Dockefile
FROM ruby:3.2.1 RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ apt-get update && apt-get install -y nodejs yarn default-mysql-client libpq-dev vim WORKDIR /myapp COPY Gemfile /myapp/Gemfile COPY Gemfile.lock /myapp/Gemfile.lock RUN gem update --system RUN bundle update --bundler RUN gem pristine --all RUN bundle install COPY . /myapp # Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 # Start the main process. CMD ["rails", "server", "-b", "0.0.0.0"]
docker-compose.yml
version: '3.7' services: db: image: mysql:8.0 platform: linux/x86_64 command: --default-authentication-plugin=mysql_native_password ports: - "3306:3306" volumes: - db:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: password security_opt: - seccomp:unconfined web: build: . stdin_open: true tty: true volumes: - .:/myapp - bundle:/usr/local/bundle command: bash -c "rm -rf tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" depends_on: - db ports: - "3000:3000" volumes: db: driver: local bundle: driver: local
Gemfile
source 'https://rubygems.org' gem 'rails', '~>7.0.4'
entrypoint.sh
#!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /myapp/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@"
コマンド実行
docker-compose build docker-compose run web rails new . --force --database=mysql --skip-bundle
ここまで実行すればmyqpp配下にファイルが展開される。
config\database.yml
の内容を編集する。
default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password # パスワードの追加 host: db # ホストの情報をdocker-composeに記載したDBに変更
再度コマンド実行。
docker-compose run rails rails db:create
docker-compose up -d
http://127.0.0.1:3000/ にアクセスが可能になる。
起動時のエラー
Ignoring racc-1.6.0 because its extensions are not built. Try: gem pristine racc --version 1.6.0 /usr/local/lib/ruby/site_ruby/3.2.0/rubygems/resolver/conflict.rb:47:in `conflicting_dependencies': undefined method `request' for nil:NilClass (NoMethodError)
Dockerfileに下記を追加して更新を図ったが解決せず…
RUN gem update --system RUN bundle update --bundler RUN gem pristine --all
一度手元の環境をすべて削除したところ解消した。
docker-compose down --rmi all --volumes --remove-orphans
AWS SDK for Rubyでlocalstack + OpenSearch環境に接続する
ローカルのdocker環境に構築したlocalstack + OpenSearchにAWS SDK for Rubyを用いた接続方法です。 接続方法自体はAWS公式のデベロッパーガイドに記載されていますが、クライアントクラス化して使用します。 docs.aws.amazon.com
docker-compose.yml
# rubyを実行するコンテナに環境変数を追加 environment: OPENSEARCH_ENDPOINT: http://localstack:4566/my-custom-endpoint AWS_ACCESS_KEY_ID: dummy AWS_SECRET_ACCESS_KEY: dummy
クライアントクラス
require 'aws-sdk-opensearchservice' class OpenSearchClient def initialize end def request(http_method, api_path, document) uri = URI(endpoint + '/' + api_path) signature = generate_signature(http_method, api_path, document) http_client = Net::HTTP.new(uri.host, uri.port) http_client.use_ssl = Rails.env.production? http_client.start do |http| request = request_http_method(http_method, uri) request.body = document.to_json request['Host'] = signature.headers['host'] request['X-Amz-Date'] = signature.headers['x-amz-date'] request['X-Amz-Security-Token'] = signature.headers['x-amz-security-token'] request['X-Amz-Content-Sha256']= signature.headers['x-amz-content-sha256'] request['Authorization'] = signature.headers['authorization'] request['Content-Type'] = 'application/json' http.request(request) end end private def endpoint @endpoint ||= ENV['OPENSEARCH_ENDPOINT'] end def request_http_method(http_method, uri) case http_method when 'GET' Net::HTTP::Get.new(uri) when 'POST' Net::HTTP::Post.new(uri) when 'PUT' Net::HTTP::Put.new(uri) when 'DELETE' Net::HTTP::Delete.new(uri) end end def generate_signature(http_method, api_path, document) signer.sign_request( http_method: http_method, url: endpoint + '/' + api_path, body: document.to_json ) end def signer @signer ||= Aws::Sigv4::Signer.new( service: 'es', region: 'ap-northeast-1', access_key_id: ENV['AWS_ACCESS_KEY_ID'], secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'], ) end end
インデックスの作成
document = { settings: { index: { analysis: { tokenizer: { kuromoji_tokenizer: { type: 'kuromoji_tokenizer' } }, analyzer: { analyzer: { type: 'custom', tokenizer: 'kuromoji_tokenizer' } } } } } } OpenSearchClient.new.request('PUT', 'sample-index', document) => "{\"acknowledged\":true,\"shards_acknowledged\":true,\"index\":\"sample-index\"}"
docker-composeでlocalstack + opensearchの環境構築
Amazon OpenSearch Serviceとの連携を行う要件に対して開発を行う場合、クラウド環境との疎通を行う前に、docker開発環境内でlocalstackを用いて検証することができる。
設定ファイル
docker-compose.yml
version: '3.7' services: opensearch: build: context: . dockerfile: Dockerfile environment: - node.name=opensearch - cluster.name=opensearch-docker-cluster - discovery.type=single-node - bootstrap.memory_lock=true - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" - "DISABLE_SECURITY_PLUGIN=true" ports: - "9200:9200" ulimits: memlock: soft: -1 hard: -1 volumes: - opensearch:/usr/share/opensearch/data localstack: image: localstack/localstack:latest ports: - "4566:4566" depends_on: - opensearch environment: - OPENSEARCH_CUSTOM_BACKEND=http://opensearch:9200 - DEBUG=${DEBUG- } - PERSISTENCE=${PERSISTENCE- } - DOCKER_HOST=unix:///var/run/docker.sock - HOSTNAME_EXTERNAL=localstack volumes: - ./docker/localstack/:/docker-entrypoint-initaws.d # 起動時に実行する初期化ファイル volumes: opensearch: driver: local
Dockerfile
FROM opensearchproject/opensearch:latest RUN /usr/share/opensearch/bin/opensearch-plugin install analysis-kuromoji RUN /usr/share/opensearch/bin/opensearch-plugin install analysis-icu
カスタムドメイン作成用シェル
./docker/localstack/init.sh
ファイルを作成./docker/localstack/:/docker-entrypoint-initaws.d
の設定で起動時に実行したい内容を記載
- docker-compose終了時に作成したドメインが消えてしまうため、起動時に毎回ドメインを作成する
awslocal opensearch create-domain \ --domain-name my-domain \ --domain-endpoint-options '{ "CustomEndpoint": "http://localstack:4566/my-custom-endpoint", "CustomEndpointEnabled": true }'
起動確認
docker exec -it [localstackコンテナ名] /bin/bash curl http://localhost:4566/my-custom-endpoint/_cluster/health?pretty { "cluster_name" : "opensearch-docker-cluster", "status" : "green", "timed_out" : false, "number_of_nodes" : 1, "number_of_data_nodes" : 1, "discovered_master" : true, "discovered_cluster_manager" : true, "active_primary_shards" : 0, "active_shards" : 0, "relocating_shards" : 0, "initializing_shards" : 0, "unassigned_shards" : 0, "delayed_unassigned_shards" : 0, "number_of_pending_tasks" : 0, "number_of_in_flight_fetch" : 0, "task_max_waiting_in_queue_millis" : 0, "active_shards_percent_as_number" : 100.0 }
Rails6 + Webpacker環境のVue.js3導入と、Viewからpropsへの受け渡し
環境
※Docker Desktop環境
Vueのインストール
yarn add vue@next vue-loader@next @vue/compiler-sfc
package.json
{ "name": "myapp", "private": true, "dependencies": { "@rails/actioncable": "^6.0.0", "@rails/activestorage": "^6.0.0", "@rails/ujs": "^6.0.0", "@rails/webpacker": "5.4.3", "@vue/compiler-sfc": "^3.2.26", // 追加項目 "turbolinks": "^5.2.0", "vue": "^3.2.26", // 追加項目 "vue-loader": "^17.0.0", // 追加項目 "webpack": "^4.46.0", "webpack-cli": "^3.3.12" }, "version": "0.1.0", "devDependencies": { "webpack-dev-server": "^3" } }
config\webpack\environment.jsの編集
初期状態
const { environment } = require('@rails/webpacker') module.exports = environment
編集後
const { environment } = require('@rails/webpacker') const path = require('path') const { VueLoaderPlugin } = require('vue-loader') const { DefinePlugin } = require('webpack') // アプリケーションのalias const customConfig = { resolve:{ alias: { "@": path.resolve(__dirname, '..', '..', 'app/javascript') } } } environment.plugins.prepend( 'VueLoaderPlugin', new VueLoaderPlugin() ) environment.loaders.prepend('vue', { test: /\.vue$/, use: [{ loader: 'vue-loader' }] }) environment.plugins.prepend( 'Define', new DefinePlugin({ __VUE_OPTIONS_API__: false, __VUE_PROD_DEVTOOLS__: false }) ) environment.config.merge(customConfig) module.exports = environment
config\webpacker.ymlの編集
- extensions: - .vue # 追加
Controller、View、JavaScriptの作成
config\routes.rb
Rails.application.routes.draw do resources :users end
app\controllers\users_controller.rb
class UsersController < ApplicationController def index @users = [{ id: 1, name: 'hoge' }, { id: 2, name: 'fuga' }] end end
app\views\users\index.html.erb
<div id="user" data-users="<%= @users.to_json %>"> <ul> <li v-for="item in items"> {{ item.name }} </li> </ul> </div> <%= javascript_pack_tag('users') %>
app\javascript\packs\users.js
import { createApp } from 'vue/dist/vue.esm-browser' import Index from '@/users/index' document.addEventListener('DOMContentLoaded', () => { // カスタムデータからの値取得と、propsへの受け渡し const { users } = document.getElementById('user').dataset createApp(Index, { users: JSON.parse(users) }).mount('#user') })
app\javascript\users\index.js
export default { props: { users: { type: Array, default: [], } }, data() { return { items: [] } }, mounted() { this.items = Object.assign({}, this.users) } }