Edited at

[Playframework]Evolutionsのファイル名を連番SQLじゃなくする

PlayframeworkではDB Migration として、Evolutionsを採用しています


参考: https://www.playframework.com/documentation/2.7.x/Evolutions

これ自体は普通に便利なのですが、SQLファイル名が 1.sql 2.sql といった連番SQLであるために、

複数開発者の間でよくSQLファイルがコンフリクトするといったことがありました。

そこで、1_add_table.sql 1_alter_columns_hogehoge.sql といったように、数字以外の文言も

SQLファイルに含められるようにしてみました


コード

Evolutionsまわりの処理はEvolutionsModuleで提供されているみたいなので、

そこの EvolutionsReader をカスタマイズしたものに変更します


EvolutiosModule.scala

import java.io.{ByteArrayOutputStream, Closeable, IOException, InputStream}

import javax.inject._
import play.Logger
import play.api.Environment
import play.api.db.evolutions._
import play.api.inject._
import play.api.libs.Collections

import scala.io.Codec

/**
* Default module for evolutions API.
*/

class EvolutionsModule extends SimpleModule(
bind[EvolutionsConfig].toProvider[DefaultEvolutionsConfigParser],
// EvolutionReaderを変更する
bind[EvolutionsReader].to[MyEnvironmentEvolutionsReader],
bind[EvolutionsApi].to[DefaultEvolutionsApi],
bind[ApplicationEvolutions].toProvider[ApplicationEvolutionsProvider].eagerly
)

/**
* Read evolution files from the application environment.
*/

@Singleton
class MyEnvironmentEvolutionsReader @Inject() (environment: Environment) extends ResourceEvolutionsReader {

/**
* Not used
*/

def loadResource(db: String, revision: Int): Option[InputStream] = {
Option.empty
}

override def evolutions(db: String): Seq[Evolution] = {

val upsMarker = """^#.*!Ups.*$""".r
val downsMarker = """^#.*!Downs.*$""".r

val UPS = "UPS"
val DOWNS = "DOWNS"
val UNKNOWN = "UNKNOWN"

val mapUpsAndDowns: PartialFunction[String, String] = {
case upsMarker() => UPS
case downsMarker() => DOWNS
case _ => UNKNOWN
}

val isMarker: PartialFunction[String, Boolean] = {
case upsMarker() => true
case downsMarker() => true
case _ => false
}

val folder = environment.getFile(Evolutions.directoryName(db))
val sqlFiles = folder.listFiles()

// 数字が先頭についているSQLファイルのみをEvolutionの対象にする
.filter(file => file.getName.matches("^[0-9]+.*\\.sql"))

// 先頭の数字順になるように
.sortBy(file => {
file.getName.split("\\.")(0).split("_")(0).toInt
})
.toSeq

sqlFiles.zip(1 to sqlFiles.size)
.map {
case (file, revision) => {
val script = readStreamAsString(file.toURI.toURL.openStream())

val parsed = Collections.unfoldLeft(("", script.split('\n').toList.map(_.trim))) {
case (_, Nil) => None
case (context, lines) => {
val (some, next) = lines.span(l => !isMarker(l))
Some((next.headOption.map(c => (mapUpsAndDowns(c), next.tail)).getOrElse("" -> Nil),
context -> some.mkString("\n")))
}
}.reverse.drop(1).groupBy(i => i._1).mapValues { _.map(_._2).mkString("\n").trim }
Evolution(
revision,
parsed.getOrElse(UPS, ""),
parsed.getOrElse(DOWNS, "")
)
}

}

}

/**
* Read the given stream into a byte array.
*
* Closes the stream.
*/

private def readStream(stream: InputStream): Array[Byte] = {
try {
val buffer = new Array[Byte](8192)
var len = stream.read(buffer)
val out = new ByteArrayOutputStream() // Doesn't need closing
while (len != -1) {
out.write(buffer, 0, len)
len = stream.read(buffer)
}
out.toByteArray
} finally closeQuietly(stream)
}

/**
* Read the given stream into a String.
*
* Closes the stream.
*/

def readStreamAsString(stream: InputStream)(implicit codec: Codec): String = {
new String(readStream(stream), codec.name)
}
/**
* Close the given closeable quietly.
*
* Logs any IOExceptions encountered.
*/

def closeQuietly(closeable: Closeable) = {
try {
if (closeable != null) {
closeable.close()
}
} catch {
case e: IOException => Logger.warn("Error closing stream", e)
}
}
}



application.conf

play.modules {

enabled += "jp.hogehoge.EvolutionsModule"
disabled += "play.api.db.evolutions.EvolutionsModule"
}



参考: https://stackoverflow.com/questions/43093432/is-it-possible-to-name-play-evolution-sql-scripts