Ruby on RailsでSQLファイルを外に書きだす方法が意外と無い。

そもそも需要が無いのかもしれない。


なので仕方なく、SQLを外だしにするコードを書いたのだが・・・

あれから約一年。未だに需要が無いのかプラグインらしきものが見つからない。

と言う事で、僕のように困ってる人のヒントになれればと思って僕のコードを公開してみます。もし需要があれば僕自身用に作ったプラグインを公開してみます。


ちなみに・・・

コードの詳しい説明は割愛しますが、簡単に挙動を書いておきます。

(久々に技術系の記事書くなぁ。。。)



-------------------------------------------------

DBと密接に関係するのはモデルであるため、モデルにDB毎のコードを書けるようにしました。
例えば、「database.yml」の「adapter」に「mysql」と記載していた場合に「User」モデルでDB毎に違うSQLを発信したいとします。

app\models\mysql\user\


この場合は上記のフォルダを作成してください。
modelsまでは設定ファイルで変更することが可能です。
その下のフォルダはアダプター毎に変更する必要があります。
今回の場合は「mysql」なので「mysql」フォルダを作成します。

そして、最後に「User」モデルに対しては「user」(小文字)フォルダを作成します。
これで格納する配置準備が出来ました。

注意:本コードは内部的にダブルクォーテーション(")を使用しているため、SQL内部ではダブルクォーテーション(")を使用しないでください。
※シングルクォーテーション(')を使用してください。

■find_sql_***

さて、その次にtest.sqlを作成したとします。

[test.sql]
select * from user


※他の処理との相性が悪くなる場合があるので、最後に「;」は付けない事を推奨。
これを先ほどの箇所に格納すると

1 [controller]
2 user = User.find_sql_test
3 user.each{ |u|
4 
5 }


と言うコードが作成されます。戻り値は必ず配列型になります。
(※find_by_sqlを使用しているので、挙動はfind_by_sqlと同様になります。) また、バインド変数を使用する場合は

[test.sql]
select * from user where id = :id


として、

1 [controller]
2 user = User.execute_sql_test({:id => 1})
3 user.each{ |u|
4 
5 }


としてください。

■execute_sql_***

find_sql_***と同様にexecute_sql_***も存在します。
処理はSQLを発行する場合に使用してください。

[test.sql]
update users set mail_address = :test


1 [controller]
2 User.execute_sql_test2({:test => 'test'})
3 
4 }

のように使用してください。

■execute_rb_***

DB毎にプログラムを変えたい場合があると思います。
まずは、test.rbを上記同様userフォルダの下に配置します。
その場合は以下のように記述してください。

1 [test.rb]
2 return "test" 


def等の関数宣言は要りません。
これを呼び出す場合は

1 [controller]
2 p User.execute_rb_test


とします。 もし、引数を渡したい場合は
呼び出し側は

1 [controller]
2 p User.execute_rb_test({:id => "テスト"})


と記載することで値を渡す事が出来ます。 受け取り側は

1 [test.rb]
2 return hash[:id] + "だよ!" 


で使用できます。(hashというインスタンスを用意しております。)




ModelにMix-inして使ってください。

Module名は適当に付けてincludeしてください。


以下、コードになります。

----------------------------------------------------------


##
# SQL格納フォルダを取得します。
#
MODEL_ADAPTER = SQL_FOLDER + read_yml_file("config/database.yml")[RAILS_ENV]["adapter"] + "/"


##
# ファイルの読み込みコストを考えてSQLファイル等をメモリキャッシュする。
#
MODEL_SQL = {}


##
# YML情報ファイル読み込み
#
def read_yml_file(fileName)
# 初期化されていない場合は初期化する。
@ymlhash = {} unless @ymlhash

# 同じファイルは読みこまない。
return @ymlhash[fileName] if @ymlhash[fileName]

# YMLファイルの読み込み
@ymlhash[fileName] = YAML::load_file(fileName)
end

module Model


##
# インクルード時の処理
#
def self.included(base)

# モデルにSQLファイルを付加する。
set_sql(base)

end


private


##
# SQLファイルをモデルに設定します。
#
def self.set_sql(base)
base_dir = MODEL_ADAPTER + base.name.underscore
unless MODEL_SQL[base.name]
# 一度読み込んだファイルは二度と読み込まないようにハッシュを定義する。
MODEL_SQL[base.name] = {}

# ディレクトリの存在チェックを行う。
Dir::entries(base_dir).each{ |file_name|
# 拡張子がSQLとRBのもののみ読み込む
extension = self.get_file_extension(file_name)
if extension == "sql" || extension == "rb"
# ファイルを読み込みハッシュに格納します。
open(base_dir + "/" + file_name) {|f|
MODEL_SQL[base.name][file_name] = f.read
}
end
} if FileTest.exist?(base_dir) && File::ftype(base_dir) == "directory"
end


# 定義されているメソッドを追加します。
MODEL_SQL[base.name].each {|hash|
extension = get_file_extension(hash[0])
method_name = get_filename_noextension(hash[0])

if extension == "sql"
extend_code = %-
class << base
def find_sql_#{method_name}(hash = {})
return self.find_by_sql(["#{hash[1]}", hash])
end

def execute_sql_#{method_name}(hash = {})
return connection.execute(sanitize_sql(["#{hash[1]}", hash]))
end
end
-
elsif extension == "rb"
extend_code = %-
class << base
def execute_rb_#{method_name}(hash = {})
#{hash[1]}
end
end
-
end
# p extend_code
eval extend_code
}

end


##
# ファイル拡張子を返却
#
def self.get_file_extension(fileName)

# 拡張子よりも短いファイル名は不正
return nil unless fileName.rindex(".")

# 拡張子(小文字)を取得する。
extension = (fileName[fileName.rindex(".") + 1..fileName.length]).downcase

# 拡張子を返却します
return extension
end


##
# ファイル拡張子以外を返却
#
def self.get_filename_noextension(fileName)

# 拡張子よりも短いファイル名は不正
return fileName unless fileName.rindex(".")

# 拡張子以外を取得する。
return (fileName[0..fileName.rindex(".") - 1])
end

end

----------------------------------------------------------


他に良い方法があったら教えてください。

一応これで、SQLファイルを隠ぺいした上に普通のメソッドを呼び出すようにSQLファイルを読み込んで実行できます。

また、キャッシュも効いているのでSQLファイルを何度も読みだす事はありません。


P.S.

フォルダの形式はDOMAっぽくしたいと最近思うようになりました。。。

必要があれば変えるかもしれません。


PostgreSQLを使ってる人で、開発環境がWindowsで本番環境はLinuxの人は一工夫が必要なので注意。