【要注意】Rails 6 + Bootstrap 4 + FileMaker Server 18 + Rfm/ginjo 3


Why?

 なぜ今さら Rfm?と思うでしょうが、今回は、FileMakerのカスタムWeb公開機能でデータベース単体として使えば(トラフィック制限はあるけど)接続クライアント数のライセンス費用はかからないので、Railsとの組み合わせはどんなもんなのか、この眼で確かめてみたかったというのが目的でした。RESTでやってみたかったけれど、Railsに匹敵するようなフレームワークを探せなかった。

 コメント欄参照していただきたいのですが、HAZIさんより情報いただきました。Rfm/ginjo はプルリクに反応なく、ruby2.6以降で警告多発し、FileMakerServer19で不安定とのことです。私もこの記事での動作確認までで、これ以上のことはしていません。

画面サンプル

 何の特徴もない画面ですが、自分には動いたということがまずささやかな実績。

Rails,Bootstrap,FimeMaker,Rfm/ginjo
一覧画面(デフォルト表示画面)


Rails,Bootstrap,FimeMaker,Rfm/ginjo
詳細画面(一覧でタイトルをクリックした後の画面)

Rails,Bootstrap,FimeMaker,Rfm/ginjo
編集画面(詳細画面で編集ボタンをクリックした後の画面)

Rails,Bootstrap,FimeMaker,Rfm/ginjo
編集(スクロール後の状態)

Rails,Bootstrap,FimeMaker,Rfm/ginjo
検索結果(一覧画面で検索ボタンをクリックした後の画面)

Rails

 Railsは初だったのでこの本で勉強。


 ※リンクは正誤表などが掲載されているサポートサイトです。

 Rails5.2対応とあり、実際はRails6でやろうとしてたのでパスかなと思いましたが、本を手にとって見ると、共通化やチーム作業などに触れられており、全体のバランスが良さそうだったのでこれに決めました。Bootstrap もこの本の手順に沿って入れました。

 Rails 6.0.2.2

Rfm/ginjo

 ginjo/rfm 3.08

 "Latest commit on 16 Dec 2017 " とあるので、2年ほど前が最後のコミットですね。
issueが20件あります。私も1件あげました。検索の結果が0件の場合に割り込みが発生しないという報告と改善案です。(ginjoさん見てくれるだろうか。)

 Download & Installationの通りに実行してインストール。

 gem install 'ginjo-rfm'

FileMaker Server

 FileMaker Server 18.0.4

 もう19が出てしまいましたが。
 ポート3000番をFileMakerServerで使っているので、Railsはポート変えて立ち上げる必要があります。
 ここではFileMaker Serverについてのインストール解説はしません。知ってる前提。

 カスタム Web 公開機能( with XML )を有効にします。CLI(コマンドラインインターフェイス)でしか有効にできないので注意。


AdminConsoleの例

データベースの例

ディレクトリ構造

viewsのみ

ソース

environment.rb
# Load the Rails application.
require_relative 'application'

# Initialize the Rails application.
Rails.application.initialize!

# FileMaker Server Object config
FM_CONFIG = {
:host => "127.0.0.1",
# :port => 16014,
:port => 16020,
:account_name => "admin",
:password => "XXXXXXXXXX",
:database => "FMServer_Sample",
:raise_on_401 => true,
:ssl => false
}

models

art.rb
require 'active_model'

class Art < Rfm::Base
config :layout => 'English_Form_View',
:host => "127.0.0.1",
:port => 16020,
:account_name => "admin",
:password => "XXXXXXXXXX",
:database => "FMServer_Sample",
:raise_401 => true,
:ssl => false,
:FMSdomain => "http://localhost"
end

views

list.html.slim
h1 一覧

.mb-3
.nav.justify-content-end
= form_tag(:action => :search, :id => params[:id]) do
.form-inline
.div.form-row
= text_field_tag "words[Search]", "", {class: 'form-control'}
| &nbsp;
= submit_tag "検索", class: 'btn btn-primary'
.mb-3
= link_to '新規登録', {:action => :new} , class: 'btn btn-primary'

.mb-3
table.table.table-hover
thread.thead-default
tr
th= "Title"
th= "Cover Photo Credit"
th= "Status"
th[align="right"]= "Quantity in Stock"
tbody
- @records.each_with_index do |record,i|
tr
td= link_to((record["Title"]), :action => :show, :id => record.record_id)
td= record["Cover Photo Credit"]
td= record["Status"]
td[align="right"]= record["Quantity in Stock"].to_i

show.html.slim
h1 詳細

.mb-3
.nav.justify-content-end
= link_to '一覧', {:action => :list}, class: 'btn btn-primary'

.mb-3
table.table.table-hover
tbody
tr
th= "Img"
td= image_tag @record["Img"] ,:width => '256' if @record["Img"]
tr
th= "Title"
td= h @record["Title"]
tr
th= "Status"
td= h @record["Status"]
tr
th= "Author"
td= h @record["Author"]
tr
th= "Publisher"
td= h @record["Publisher"]
tr
th= "Photo Credit"
td= h @record["Cover Photo Credit"]
tr
th= "Quantity in Stock"
td= h @record["Quantity in Stock"].to_i
tr
th= "Number of Pages"
td= h @record["Number of Pages"].to_i
tr
th= "Description"
td= simple_format(h(@record["Description"]), {}, sanitize: false, wrapper_tag: "div")

= link_to '編集', {:action => :detail, :id => @record.record_id}, class: "btn btn-primary mr-3"

detail.html.slim
h1 編集

.nav.justify-content-end
= link_to "一覧", {:action => :list}, class: "btn btn-primary"

.mb-3
table.table.table-hover
tbody
tr
th= "Img"
td= image_tag @record["Img"] ,:width => '256' if @record["Img"]

= form_with model: @art, url: {controller: 'art', action: 'save', :id => params[:id] } do |f|
.form-group
= f.label :Title
= f.text_field :Title, class: 'form-control', id: 'art_Title', value: "#{h @record["Title"]}"
.form-group
= f.label :Status
= f.text_field :Status, class: 'form-control', id: 'art_Status', value: "#{h @record["Status"]}"
.form-group
= f.label :Author
= f.text_field :Author, class: 'form-control', id: 'art_Author', value: "#{h @record["Author"]}"
.form-group
= f.label :Publisher
= f.text_field :Publisher, class: 'form-control', id: 'art_Publisher', value: "#{h @record["Publisher"]}"
.form-group
= f.label :'Cover Photo Credit'
= f.text_field :'Cover Photo Credit', class: 'form-control', id: 'art_Cover Photo Credit', value: "#{h @record["Cover Photo Credit"]}"
.form-group
= f.label :'Quantity in Stock'
= f.text_field :'Quantity in Stock', class: 'form-control', id: 'art_Quantity in Stock', value: "#{h @record["Quantity in Stock"]}"
.form-group
= f.label :'Number of Pages'
= f.text_field :'Number of Pages', class: 'form-control', id: 'art_Number of Pages', value: "#{h @record["Number of Pages"]}"
.form-group
= f.label :'Description'
= f.text_area :'Description', class: 'form-control', id: 'art_Description', value: "#{h @record["Description"]}"
= f.submit nil, class: 'btn btn-primary'
|
= link_to 'キャンセル', {:action => :list }, class: 'btn btn-secondary'
| &nbsp;
= link_to "削除", {:action => :destroy ,:id => params[:id]}, class: "btn btn-danger",data: {confirm: "削除しますか?"}

search.html.slim
h1 一覧

.mb-3
.nav.justify-content-end
= form_tag(:action => :search, :id => params[:id]) do
.form-inline
.div.form-row
= text_field_tag "rec[Title]", "", {class: 'form-control'}
| &nbsp;
= submit_tag "検索", class: 'btn btn-primary'
.mb-3
= link_to '新規登録', {:action => :new} , class: 'btn btn-primary'

.mb-3
table.table.table-hover
thread.thead-default
tr
th= "Title"
th= "Cover Photo Credit"
th= "Status"
th[align="right"]= "Quantity in Stock"
tbody
- @records.each_with_index do |record,i|
tr
td= link_to((record["Title"]), :action => :show, :id => record.record_id)
td= record["Cover Photo Credit"]
td= record["Status"]
td[align="right"]= record["Quantity in Stock"].to_i

new.html.slim
h1 新規作成

.nav.justify-content-end
= link_to '一覧', {:action => :list}, class: 'btn btn-primary'

= form_with model: @art, url: {controller: 'art', action: 'create' } do |f|
.form-group
= f.label :Title
= f.text_field :Title, class: 'form-control', id: 'art_Title', value: 'タイトル'
.form-group
= f.label :Status
= f.text_field :Status, class: 'form-control', id: 'art_Status', value: '状況'
.form-group
= f.label :Author
= f.text_field :Author, class: 'form-control', id: 'art_Author', value: '作者'
.form-group
= f.label :Publisher
= f.text_field :Publisher, class: 'form-control', id: 'art_Publisher', value: '提供者'
.form-group
= f.label :'Cover Photo Credit'
= f.text_field :'Cover Photo Credit', class: 'form-control', id: 'art_Cover Photo Credit', value: '提供'
.form-group
= f.label :'Quantity in Stock'
= f.text_field :'Quantity in Stock', class: 'form-control', id: 'art_Quantity in Stock', value: '在庫'
.form-group
= f.label :'Number of Pages'
= f.text_field :'Number of Pages', class: 'form-control', id: 'art_Number of Pages', value: 'ページ数'
.form-group
= f.label :'Description'
= f.text_area :'Description', class: 'form-control', id: 'art_Description', value: '詳細'
= f.submit nil, class: 'btn btn-primary'
|
= link_to 'キャンセル', {:action => :list }, class: 'btn btn-secondary'

controllers

art_controller.rb
require "rfm"

class ArtController < ApplicationController

def list
begin
# flash[:notice] = ""
if params[:id] == nil && ( params[:Search] == nil || params[:Search] == "")
@records = Art.all
elsif params[:id] != nil && ( params[:Search] == nil || params[:Search] == "")
@records = Art.all :sort_field => params[:id]
elsif params[:Search] != ""
@records = Art.find "TextForSearch" => params[:Search]
flash[:notice] = "'#{params[:Search]}' で検索しました。"
end
rescue Rfm::Error::NoRecordsFoundError
flash[:notice] = "'#{params[:Search]}' で検索しましたが、対象は0件でした。"
@records = Art.all
end
end

def search
redirect_to :action => :list, :words["Search"] => search_params
end

def new
@art = Art.new
end

def create
begin
Art.create( edit_params.to_h )
flash[:notice] = "レコードを作成しました。"
@records = Art.all
redirect_to :action => :list
rescue Rfm::Error::ValidationError
render_text "A record with id '#{params[:id]}' ValidationError.", :status => 0
end
end

def show
begin
@record = Art.find params[:id]
@record["Img"] = Art.config[:FMSdomain].to_s + ":" + Art.config[:port].to_s + "#{@record["Img"]}"
if @record == nil
flash[:notice] = "該当レコードがありません。"
@records = Art.all
redirect_to :action => :list
end
rescue Rfm::Error::NoRecordsFoundError
render_text "A record with id '#{params[:id]}' could not be found.", :status => 401
end
end

def detail
begin
@art = Art.new
@record = Art.find(params[:id])
@record["Img"] = Art.config[:FMSdomain].to_s + ":" + Art.config[:port].to_s + "#{@record["Img"]}"
rescue Rfm::Error::RecordMissingError
render_text "A record with id '#{params[:id]}' could not be found.", :status => 404
end
end
def save
Art.edit params[:id], edit_params
flash[:notice] = "更新しました。"
redirect_to(:action => :detail, :id => params[:id])
end

def destroy
begin
@record = Art.find params[:id]
Art.delete params[:id]
flash[:notice] = "レコードを削除しました。"
rescue Rfm::Error::RecordMissingError
flash[:notice] = "レコードを削除できませんでした。"
end
redirect_to :action => :list
end

private

def edit_params
params.require(:art).permit(:"Title", :"Status", :"Author", :"Publisher", :"Cover Photo Credit", :"Quantity in Stock", :"Number of Pages", :"Description", :"Img")
end

def search_params
params.require(:words).permit(:"Search")
end

end

感想

 初めてのRails体験は率直に面白かった。MVCの思想はシステムの構成だけでなく,開発者のチーム編成、役割分担にもシンクロするだろう。
 FileMakerの使い方としては、データ参照のみの人がたくさんいる場合にブラウズはRailsで、データ入力、加工、運用する人がごく一部ならFileMakerのクライアントまたはユーザーライセンスを使う形態といったところでしょうか。
 しかし、これだけの基本的な動きも、FileMakerをそのまま使うならもっと簡単ですね。改めてFileMakerの強力な機能を思い知りました。

参考にした情報

Rfm

ありがとうございました!!!

以上

2 件のコメント:

  1. つい最近 Rfm を捨てたので思わずコメントします。

    Rfm 開発止まっててPR投げてもマージしてくれない状況です(そんな状況なのでruby 2.6 以降では警告が大量に出ます)。あと、FileMaker Server 19 では Custom Web(XML) は微妙に不安定です。一定量を超えるリクエストを投げるとステータス200なのにデータが空で返ってきて Rfm がエラーはきます。

    なので、業務で使う場合はもう容量制限はありますが Data API (JSON) を使うしかないのかなという状況です。fmrest gem を使うと Rfmと似たような感じでData APIを扱えます。
    https://github.com/beezwax/fmrest-ruby

    返信削除
  2. あ~そうですか。残るはRESTしかないか。本文にも注意書きしておきます。ありがとうございました。

    返信削除