Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

tamanugi
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away