Grails
Groovy

[Grails]アップロードフォームを扱い、変数の型についても少し考える(複数ファイルも対応)

More than 3 years have passed since last update.

参考

MultipartFile(spring)
CommonsMultipartFile(spring)

概要

タイトル通り、HTMLからアップロードされるファイルを保存する方法についてまとめました。
アップロードされたファイルを、/tmp/uploadディレクトリに、アップロードされたファイル名のまま保存するサンプルです。

一つのファイルのみアップロードする場合

オーソドックスなタイプです。
GSPは以下。

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
  <title></title>
</head>
<body>
<g:uploadForm controller="test" action="uploadFile">
    <input type="file" name="myFile" /><br />
    <g:submitButton name="go" value="go" />
</g:uploadForm>
</body>
</html>

このリクエストを受け付けてファイルを保存するコントローラが以下。

def uploadFile() {
    MultipartFile file = request.getFile("myFile")
    if(!file.isEmpty()) {
        file.transferTo( new File("/tmp/upload/${file.originalFilename}") )
        render "OK"
    } else {
        render "NG"
    }
}

ソースを見ていただければなんとなく分かると思いますが、request.getFile('アップロードフィールド名')を実行すれば、アップロードされたファイルの実体をMultipartFileとして取得できます。
アップロードフィールドに何も指定されていなかった場合を考慮して、isEmpty()を使って処理を切り分けることができます。

不特定多数のファイルをアップロードする場合

こっちのほうが本命でした。(自分にとって)
今日、JavaScriptなどによって特定のHTMLページからアップロードされるファイル数は動的に変化するようになりました。
Grailsで不特定多数のファイルをアップロードするにはどうればいいのか、それが以下になります。

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
  <title></title>
</head>
<body>
<g:uploadForm controller="test" action="uploadFileMulti">
    <!--同名のアップロードフィールドを定義する。-->
    <input type="file" name="myFile" /><br />
    <input type="file" name="myFile" /><br />
    <input type="file" name="myFile" /><br />
    <g:submitButton name="go" value="go" />
</g:uploadForm>
</body>
</html>
def uploadFileMulti(){
    request.getFiles("myFile").findAll { MultipartFile file ->
        !file.isEmpty()
    }.each { MultipartFile file ->
        file.transferTo( new File("/tmp/upload/${file.originalFilename}"))
    }

    render "OK"
}

偉そうなことを言いつつ、やっていることに変化はありません。
アップロードされたファイルを取得するrequest.getFile()request.getFiles()に変わっただけです
コレはMultipartFileの実体が入ったリストを返してきますので、Groovyの便利なコレクション系メソッドを使って処理してあげます。
request.getFiles()を実行するとsubmitされた時にformの中に存在していた<input type="file">の数だけMultipartFileのインスタンスが生成されます。
しかし、すべての<input type="file">にファイルが指定されていないことは当然ありえるので、ここでもちゃんとisEmpty()を判定しています。
これで、JavaScriptによって動的にアップロードされるファイル数が変動しても問題なく対応できます。
(当然異常な数のアップロードを実行されると不味いのでその対策とかは必要ですがそれは別の話)

注意点としては、HTML/GSPで<input type="file">を定義する際に、すべて同じnameを割り当てる必要があるという点です。(今回の場合はmyFileというnameを指定)
そうしておけば。同じnameが指定された<input type="file">request.getFiles()で一度にすべて取得できます。

MultipartFile型の変数について

実はMultipartFile自体はインタフェースで、request.getFile()request.getFiles()が返してくるのはMultipartFileインタフェースを実装したCommonsMultipartFileのインスタンスになります。
なので、変数宣言時に型も宣言する場合、CommonsMultipartFileを指定しても当然動作します。

ただしCommonsMultipartFileを使うとユニットテストができなくなります!

Grailsでファイルのアップロードをテストする際のテストコードを以下に示します。

TestSpeck.groovy
package パッケージ名

import grails.test.mixin.TestFor
import org.codehaus.groovy.grails.plugins.testing.GrailsMockMultipartFile

@TestFor(TestController)
class TestControllerSpec extends Specification {

    void "ファイルアップロードテスト"() {
        when:
        GrailsMockMultipartFile file = new GrailsMockMultipartFile('myFile', 'test.jpg', 'image/jpeg', 'some file data'.bytes)
        request.addFile file
        controller.uploadFile()

        then:
        file.targetFileLocation.path == '/tmp/upload/test.jpg'
        response.text == "OK"
    }
}

味噌はモックであるGrailsMockMultipartFileです。
このコードを実行すると以下のエラーが発生します。

org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'org.codehaus.groovy.grails.plugins.testing.GrailsMockMultipartFile@643d449a' with class 'org.codehaus.groovy.grails.plugins.testing.GrailsMockMultipartFile' to class 'org.springframework.web.multipart.commons.CommonsMultipartFile'

GrailsMockMultipartFileはCommonsMultipartFileにはキャストできないよーというエラーです。
なので、テストを確かに実施するためにも、request.getFile(s)()の実行結果を格納する変数はMultipartFile型で宣言してあげるべきです。

なぜ変数に型を明示的に指定しているか

Groovyなので、当然defで宣言することもできます。
また、そうしておけば上記のユニットテストに関する問題もそもそも起こりえません。
じゃあなぜ型も宣言するのかというと安全性とドキュメントのためです。
安全性というと少し大げさですが、Groovyの場合、変数に型を宣言しておけば、当然マッチしない値をその変数に入れようとするとエラーになります。(Groovyの場合実行時にです)
JavaやScalaと違ってコンパイル時点では型に関するエラーは原則捕捉できませんが、実行時にエラーになるため、その変数の値が本当に意図した型の値が入っているかというチェックをコード中に書く必要がなくなります。
コレはクロージャやメソッドの引数で効果てきめんです。
ドキュメントとしてという部分はもうそのままです。
明示的な型宣言の無い言語の場合、変数名やコメントでその値の型をドキュメントとすることがありますが、Groovyの場合は型宣言ができ、その変数には本当に合致する型の値しか格納できないので、仕様が変わって値の型が異なるものになった際にコメントや変数名の修正漏れというミスが防げます。

とはいえ毎回毎回型を書くとソースがごちゃごやしてしまうので、私の場合は以下のルールを設けています。

  1. クロージャ、メソッドの引数は必ず型も宣言する
  2. それ以外の変数の場合、明らかに格納される値が分かるものはdef、分かりづらい場合は型を宣言する

としています。
2番は微妙なところですが、例えばGrailsのGORMでDomain.findBy...みたいなコードの場合、明らかにDomain型の値が帰ってくるので、その場合はdefで宣言しています。

その他

uploadFileMulti()が関数型っぽく書けた気がする!

JavaやScalaと違ってコンパイル時点では型に関するエラーは原則捕捉できません

と書きましたが、あくまで原則です。Groovyには静的型チェックや静的コンパイルといった機能があり、コレを利用するとある程度厳密に型に関するチェックをコンパイラがやってくれたりします。
ただGrailsのように動的に色々拡張される場合にはなかなか有効に利用できないのかも。。。?