20200321のMySQLに関する記事は5件です。

Slick3.3以降でMySQLのtimestamp等の日付関連をLocalDateTimeへマッピングする

Slick3.3以降でMySQLのtimestamp等の日付関連をLocalDateTimeへマッピングする

Slick3.3からjava.time系のデータ型へのサポートが追加されたのですが、その時のデータの扱い方がRDBによって大きく差がある状態になっています。

特にMySQLはほとんどのデータがStringとして扱われているため、slick3.2以下で行っていたようなjava.sql.TimestampLocalDateTimeのMappedColumnTypeを利用したものが動作しなくなっちゃいました。

対応方法はいくつかあるのですが、参考になる情報がウェブ上に多くないため誰かの参考になればと思い記事にしてみます。

一番良いと思っている方法をメインで紹介しますが、他の方法についてもおまけ的に後ろの方に記載しようと思います。

公式推奨実装

最初に結論を書いてしまいます。

公式サイトが英語なので今まで読み飛ばしてしまっていたのですが、公式が推奨の対応方法を書いてくれていました。

記載があるのはさっきのリンクと同じここなのですが、具体的には以下の部分です。

If you need to customise these formats, you can by extending a Profile and overriding the appropriate methods. For an example of this see: https://github.com/d6y/instant-etc/blob/master/src/main/scala/main.scala#L9-L45. Also of use will be an example of a full mapping, such as: https://github.com/slick/slick/blob/v3.3.0/slick/src/main/scala/slick/jdbc/JdbcTypesComponent.scala#L187-L365.

Profileを拡張しなさい、って書いてありますよね。

今回で言うとMySQLProfileを継承して日付関連のマッピングをoverrideすれば良いと言うことですね。

では、エラーの原因を追いつつ実装をしてみます。

最終的な実装だけ知りたい方はこちらから飛んでください。

単純にマッピングを行おうとした場合に発生するエラー調査

実装の前に、そもそもどんなエラーになってしまうかを説明します。

実際に出力したエラーログは以下です。

play.api.http.HttpErrorHandlerExceptions$$anon$1: Execution exception[[DateTimeParseException: Text '2020-03-15 13:15:00' could not be parsed at index 10]]
        at play.api.http.HttpErrorHandlerExceptions$.throwableToUsefulException(HttpErrorHandler.scala:332)
        at play.api.http.DefaultHttpErrorHandler.onServerError(HttpErrorHandler.scala:251)
        at play.core.server.AkkaHttpServer$$anonfun$2.applyOrElse(AkkaHttpServer.scala:421)
        at play.core.server.AkkaHttpServer$$anonfun$2.applyOrElse(AkkaHttpServer.scala:417)
        at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:453)
        at akka.dispatch.BatchingExecutor$AbstractBatch.processBatch(BatchingExecutor.scala:55)
        at akka.dispatch.BatchingExecutor$BlockableBatch.$anonfun$run$1(BatchingExecutor.scala:92)
        at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
        at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:94)
        at akka.dispatch.BatchingExecutor$BlockableBatch.run(BatchingExecutor.scala:92)
Caused by: java.time.format.DateTimeParseException: Text '2020-03-15 13:15:00' could not be parsed at index 10
        at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)
        at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
        at java.time.LocalDateTime.parse(LocalDateTime.java:492)
        at java.time.LocalDateTime.parse(LocalDateTime.java:477)
        at slick.jdbc.MySQLProfile$JdbcTypes$$anon$4.getValue(MySQLProfile.scala:404)
        at slick.jdbc.MySQLProfile$JdbcTypes$$anon$4.getValue(MySQLProfile.scala:389)
        at slick.jdbc.SpecializedJdbcResultConverter$$anon$1.read(SpecializedJdbcResultConverters.scala:26)
        at slick.jdbc.SpecializedJdbcResultConverter$$anon$1.read(SpecializedJdbcResultConverters.scala:24)
        at slick.relational.ProductResultConverter.read(ResultConverter.scala:54)
        at slick.relational.ProductResultConverter.read(ResultConverter.scala:44)

まずわかりやすくparseエラーが出ています。

Execution exception[[DateTimeParseException: Text$ '2020-03-15 13:15:00' could not be parsed at index 10]]

index 10なので、入力された日付データでいうと この空白部分です。

空白でパースエラーとは一体...?

という、気持ちになりますね? はい、なりました。

続きが気になるので、引き続きコードを追ってみます。

次のヒントは at slick.jdbc.MySQLProfile$JdbcTypes$$anon$4.getValue(MySQLProfile.scala:404) のメッセージ。

MySQLProfileの該当箇所周辺をみてみます。

MySQLProfile.scala
override val localDateTimeType : LocalDateTimeJdbcType = new LocalDateTimeJdbcType {
  override def sqlType : Int = {
    /**
     * [[LocalDateTime]] will be persisted as a [[java.sql.Types.VARCHAR]] in order to
     * avoid losing precision, because MySQL stores [[java.sql.Types.TIMESTAMP]] with
     * second precision, while [[LocalDateTime]] stores it with a millisecond one.
     */
    java.sql.Types.VARCHAR
  }
  override def setValue(v: LocalDateTime, p: PreparedStatement, idx: Int) : Unit = {
    p.setString(idx, if (v == null) null else v.toString)
  }
  override def getValue(r: ResultSet, idx: Int) : LocalDateTime = {
    r.getString(idx) match {
      case null => null
      case iso8601String => LocalDateTime.parse(iso8601String) // <- ここです
    }
  }
  override def updateValue(v: LocalDateTime, r: ResultSet, idx: Int) = {
    r.updateString(idx, if (v == null) null else v.toString)
  }
  override def valueToSQLLiteral(value: LocalDateTime) : String = {
    stringToMySqlString(value.toString)
  }
}

// <- ここです って書いてある場所が、404行目です。

getValueではDBから取得した値をgetStringで文字列として受け取り、そのままLocalDateTime.parseに渡しているということですね。

特にフォーマットを指定していないので、parseメソッドのデフォルトフォーマットで対応できるもの以外は全て落ちてしまうような実装になっているわけですね。

ちなみにデフォルトのフォーマットがどんな感じかと言うとparse(text, DateTimeFormatter.ISO_LOCAL_DATE_TIME)こんな感じになっています。

ISO_LOCAL_DATE_TIMEyyyy-MM-ddTHH:mm:ss形式になっているので2020-03-15 13:15:00ではTが足らずにエラーになってしまうと言うわけです。

index 10でエラーになる謎が解決できましたね。

つまり、このパース処理を「いい感じ」に直してあげれば問題が解決すると言うことです!

Profileを実装

原因が判明したので公式情報に則って、MySQLProfileを拡張した独自Profileを作成していきます。

独自に作成、と聞くと難しく感じますがMySQLProfileの実装のうち必要な場所をコピペしてきて、parse処理を直すだけです。

MySQLProfileの実装はこちらです。

MySQLProfile.scala#L389-L415

では、これを参考に実装した処理を以下に記載します。

MyProfile.scala
import java.time.format.DateTimeFormatter
import java.time.LocalDateTime
import java.time.format.DateTimeFormatterBuilder
import java.time.temporal.ChronoField

/* LocalDateTimeをプロダクトに適した形に処理できるようにProfile設定を独自に拡張 */
trait MyDBProfile extends slick.jdbc.JdbcProfile with slick.jdbc.MySQLProfile {
  import java.sql.{PreparedStatement, ResultSet}
  import slick.ast.FieldSymbol

  @inline
  private[this] def stringToMySqlString(value : String) : String = {
    value match {
      case null => "NULL"
      case _ =>
        val sb = new StringBuilder
        sb append '\''
        for(c <- value) c match {
          case '\'' => sb append "\\'"
          case '"' => sb append "\\\""
          case 0 => sb append "\\0"
          case 26 => sb append "\\Z"
          case '\b' => sb append "\\b"
          case '\n' => sb append "\\n"
          case '\r' => sb append "\\r"
          case '\t' => sb append "\\t"
          case '\\' => sb append "\\\\"
          case _ => sb append c
        }
        sb append '\''
        sb.toString
    }
  }

  override val columnTypes = new JdbcTypes

  // Customise the types...
  class JdbcTypes extends super.JdbcTypes {

    // PostgresのProfileを参考にミリ秒も含めて対応できるformatterを実装
    private[this] val formatter = {
      new DateTimeFormatterBuilder()
        .append(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
        .optionalStart()
        .appendFraction(ChronoField.NANO_OF_SECOND,0,9,true)
        .optionalEnd()
        .toFormatter()
    }

    override val localDateTimeType : LocalDateTimeJdbcType = new LocalDateTimeJdbcType {
      override def sqlType : Int = {
        java.sql.Types.VARCHAR
      }

      override def setValue(v: LocalDateTime, p: PreparedStatement, idx: Int) : Unit = {
        p.setString(idx, if (v == null) null else v.toString)
      }
      override def getValue(r: ResultSet, idx: Int) : LocalDateTime = {
        r.getString(idx) match {
          case null       => null
          // 文字列から日付型にパースできるようにparseにformatterを渡す
          case dateString => LocalDateTime.parse(dateString, formatter)
        }
      }
      override def updateValue(v: LocalDateTime, r: ResultSet, idx: Int) = {
        r.updateString(idx, if (v == null) null else v.toString)
      }
      override def valueToSQLLiteral(value: LocalDateTime) : String = {
        stringToMySqlString(value.toString)
      }
    }
  }
}

object MyDBProfile extends MyDBProfile

自前でformatterを実装していますが、これはPostgreSQL用のProfileから拝借してきた実装になります。

このように実装することでDB側の日時がミリ秒まで設定されていても正常にパースが行えるようになります。

LocalDateTimeのparseはミリ秒については一律で設定する方法がなく、ミリ秒の数だけSSSSSSみたいに書かないといけないのでLocalDateTime側のメソッドで処理してもらえると助かりますね。

また0-9までの指定になっているのは、MySQL側でミリ秒以下が1つでもあると0埋めして9桁で受け取ろうとするからです。

MySQL自体は6桁までの精度しかない(はず)です。

ここで作成したprofileをslickを利用するモデルクラス等で利用してもらえば、エラーもなくマッピングができるようになります。

codegenで自動生成されたTablesファイルで恐縮ですが、こんなイメージです。

object Tables extends {
  val profile = slick.jdbc.MySQLProfile
} with Tables

trait Tables {
  val profile: slick.jdbc.JdbcProfile
  import profile.api._

  implicit def GetResultModel(implicit e0: GetResult[Long], e1: GetResult[String], e2: GetResult[LocalDateTime]): GetResult[Model] = GetResult{
    prs => import prs._
    Model.tupled((Some(<<[Long]), <<[String], <<[LocalDateTime], <<[LocalDateTime], <<[LocalDateTime]))
  }

  class Model(_tableTag: Tag) extends profile.api.Table[Model](_tableTag, Some("db"), "model") {
    def * = (id, content, postedAt, createdAt, updatedAt) <> (
      (x: (Long, String, LocalDateTime, LocalDateTime, LocalDateTime)) => {
        Model(Some(x._1), x._2 ,x._3, x._4, x._5)
      },
      (model: Model) => {
        Some((model.id.getOrElse(0L), model.content, model.postedAt, model.createdAt, model.updatedAt))
      }
    )

    def ? = ((Rep.Some(id), Rep.Some(content), Rep.Some(postedAt), Rep.Some(createdAt), Rep.Some(updatedAt))).shaped.<>({r=>import r._; _1.map(_=> Model.tupled((Option(_1.get), _2.get, _3.get, _4.get, _5.get)))}, (_:Any) =>  throw new Exception("Inserting into ? projection not supported."))

    val id:        Rep[Long]          = column[Long]("id", O.AutoInc, O.PrimaryKey)
    val content:   Rep[String]        = column[String]("content", O.Length(120,varying=true))
    val postedAt:  Rep[LocalDateTime] = column[LocalDateTime]("posted_at")
    val createdAt: Rep[LocalDateTime] = column[LocalDateTime]("created_at")
    val updatedAt: Rep[LocalDateTime] = column[LocalDateTime]("updated_at")
  }

  lazy val query = new TableQuery(tag => new Model(tag))
}

このようにモデル側では特にマッピングで何かする必要はなく、Stringなどの型と同じように利用することができます。

※ mapping以外の実装については気にしないでください。

この方法だとProfileにさえ実装を記載していれば、他のファイルではそれを意識しなくていいので非常に楽ですね。

これで実装は完了です。

独自にProfileを作成すると書くと、大変そうですがparse処理を直してるだけと言い直すと、めっちゃ簡単な気がしますよね。実際簡単でシンプルです。

おまけ

ここから先はおまけです。

すごく簡単にではありますが、他の実装方法を紹介します。

が、個人的にはお勧めしません。

MappedColumnTypeを利用した方法

通常であればslickはこの方法で外部の型をマッピングできるようにしていきます。

ただし、今回については例外でこの方法ではシンプルに実装できませんでした。

この実装についてはこちらの記事を参考にさせていただきました。

試しに作ってみた実装

実装を見た方が早いので、コードを載せます。

SlickMappedMySQLDateTime.scala
import java.time.format.DateTimeFormatter
import java.time.LocalDateTime

/* MySQLDateTimeを直接マッピング対象にできないので、mappingに利用するクラスを作成する */
case class MySQLDateTime(v: String) {
  def toLocalDateTime: LocalDateTime = LocalDateTime.parse(v, MySQLDateTime.format)
}

object MySQLDateTime {
  val format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
  def apply(time: LocalDateTime): MySQLDateTime = MySQLDateTime(time.format(format))
}

// mixinできるようにtraitにしてる。
trait LocalDateTimeColumMapper {
  val profile: slick.jdbc.JdbcProfile
  import profile.api._

  // いわゆるMappedColumnType
  implicit lazy val localDateTimeMapper = MappedColumnType.base[MySQLDateTime, String] (
    { ldt => ldt.v },
    { str => MySQLDateTime(str)}
  )
}

まずMySQLDateTimeというcase classを実装しています。

ここが使いづらい原因になっている箇所です。

本当は直接LocalDateTimeに紐づけたいのですが、MySQLProfileでLocalDateTimeへのマッピングが用意されているので先にそちらの処理が呼び出されてしまうようでした。

MappedColumnType.base[LocalDateTime, String] (
    { ldt => ldt.toString },
    { str => LocalDateTime.parse(str, formatter)}
  )

例えばこんなMappingをして、以下のように書くとどうなるか。

val createdAt: Rep[LocalDateTime] = column[LocalDateTime]("created_at")

この場合Profileに定義されているLocalDateTimeのマッピングが先に処理されて、その後にMappedColumnTypeの処理に移ろうとするみたいでした。

なので結局はLocalDateTime.parseの箇所で落ちてしまうわけですね。

かといってStringに対してのmappingにしてしまうと、普通にStringで使いたいものについても変換がかかってしまいます。

そのためLocalDateTimeを直接使うことができません。

結果、一度経由するためのクラスとして独自のMySQLDateTimeという型が必要になってしまったと言うわけです。

既に若干冗長になってしまいました。

しかし、この実装だとまたもうちょっと大変なところがあります。

モデル側の実装を見てみましょう。

Model.scala
def * = (id, content, postedAt, createdAt, updatedAt) <> (
  (x: (Long, String, MySQLDateTime, MySQLDateTime, MySQLDateTime)) => {
    Model(
      Some(x._1),
      x._2,
      x._3.toLocalDateTime,
      x._4.toLocalDateTime,
      x._5.toLocalDateTime
    )
  },
  (model: Model) => {
    Some((
      model.id.getOrElse(0L),
      model.content,
      MySQLDateTime(model.postedAt.toString),
      MySQLDateTime(model.createdAt.toString),
      MySQLDateTime(model.updatedAt.toString)
    ))
  }
)

実装を見るとわかりますが、せっかくMappedColumnTypeをしているのにモデル側でも取り回しの処理をケアしてあげないといけない状態になっています。

ここはもしかしたら私の実装の仕方に問題があるのかも? と思っていますが...

このようにMappedColumnTypeでの実装だと、必要になるコード量は増えるのに結局ケアもしないといけないということで手間が倍に増えてしまいました。

そのため今回のケースについては適切ではなさそうだなという気持ちになりました。

Stringのまま処理してモデルへの変換時に処理するパターン

次に一番シンプルな方法で実装してみます。

package slick.samples

import java.time.LocalDateTime
import slick.jdbc.{GetResult}
import java.time.format.DateTimeFormatter
import models.Model

/* def *のtupleでマッピングをするサンプル実装 */
object Tables extends {
  val profile    = slick.jdbc.MySQLProfile
} with Tables

trait Tables {
  val profile: slick.jdbc.JdbcProfile
  val format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")

  import profile.api._

  /* Slick3.3ではDATETIME, TIMESTAMPなどをStringで受け取るため、モデルとの相互変換部分で吸収する */
  implicit def GetResultModel(implicit e0: GetResult[Long], e1: GetResult[String]): GetResult[Model] = GetResult{
    prs => import prs._
    Model.tupled((
      <<[Option[Long]],
      <<[String],
      LocalDateTime.parse(<<[String], format),
      LocalDateTime.parse(<<[String], format),
      LocalDateTime.parse(<<[String], format)
    ))
  }

  /* Slick3.3ではDATETIME, TIMESTAMPなどをStringで受け取るため、モデルとの相互変換部分で吸収する */
  class Model(_tableTag: Tag) extends profile.api.Table[Model](_tableTag, Some("twitter_clone"), "model") {
    def * = (id, content, postedAt, createdAt, updatedAt) <> (
      (x: (Long, String, String, String, String)) => {
        Model(
          Some(x._1),
          x._2,
          LocalDateTime.parse(x._3, format),
          LocalDateTime.parse(x._4, format),
          LocalDateTime.parse(x._5, format)
        )
      },
      (model: Model) => {
        Some((model.id.getOrElse(0L), model.content, model.postedAt.toString, model.createdAt.toString, model.updatedAt.toString))
      }
    )

    def ? = ((Rep.Some(id), Rep.Some(content), Rep.Some(postedAt), Rep.Some(createdAt), Rep.Some(updatedAt))).shaped.<>({r=>import r._; _1.map(_=> Model.tupled((Option(_1.get), _2.get, LocalDateTime.parse(_3.get, format), LocalDateTime.parse(_4.get, format), LocalDateTime.parse(_5.get, format))))}, (_:Any) =>  throw new Exception("Inserting into ? projection not supported."))

    val id:        Rep[Long]   = column[Long]("id", O.AutoInc, O.PrimaryKey)
    val content:   Rep[String] = column[String]("content", O.Length(120,varying=true))
    val postedAt:  Rep[String] = column[String]("posted_at")
    val createdAt: Rep[String] = column[String]("created_at")
    val updatedAt: Rep[String] = column[String]("updated_at")
  }

  lazy val query = new TableQuery(tag => new Model(tag))
}

これはdef *などなど、変換が必要な場所それぞれで丁寧に処理していくパターンですね。

対応方法としてはシンプルでわかりやすいのかなと思います。

ただ、全てのモデルや全ての日付型のマッピングでコツコツ実装をしてあげないといけないので精神力が必要になります。

まとめ

今までずっとどうしたらいいのか悩んでいたのですが、やはり公式をしっかり読むのが一番ですね。

もっと良い方法をご存知の方がいたら教えていただけますと幸いです。

しかし、PostgreSQLなどは日付のparseに適切なformatterが用意されているのにMySQLではなんで日付がそのまま使われているのでしょうか。

Zoned Timestampなどが入ってきたときなど、微妙に一つのデータ型で管理されているフォーマットパターンが多すぎて対応しきれなかったのですかね??
Oracleについては、こういうところは丁寧にデータが用意されており流石なのかな? と思いました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MySQLをHomebrewでインストールして初期設定まで行ったけど、MySQLがどうしても起動しなかったときの対応

この記事は

MySQLの環境構築でけっこう手こずったので、手順とエラー対応をメモ。
トライしたことを書き出すことで、自分の中で整理したい。

手順

HomebrewでMySQLをインストール

ターミナルにて、Homebrewを最新にアップデートしてMySQLをインストールします。
インストールが成功すると下記のように出力されます。
(usernameにそれぞれのPC名が入ります)
(余談だけど、初心者だとこういう「自分と出力がちょっと違う!なぜ!?」ってなりがちな気がする。hogehogeとか最初は「へぇ???」だったw)

#Homebrewのアップデート
$ brew update

#MySQLのインストール
$ brew install mysql

Already downloaded: /Users/username/Library/Caches/Homebrew/downloads/*****--mysql-8.0.19.catalina.bottle.2.tar.gz
==> Pouring mysql-8.0.19.catalina.bottle.2.tar.gz
==> /usr/local/Cellar/mysql/8.0.19/bin/mysqld --initialize-insecure --user=username --basedir=/usr/local/Cellar/mysql/8.0.19 --datadir=/usr/local/var/mysql --tmpdir=/tmp
==> Caveats
We've installed your MySQL database without a root password. To secure it run:
    mysql_secure_installation

MySQL is configured to only allow connections from localhost by default

To connect run:
    mysql -uroot

To have launchd start mysql now and restart at login:
  brew services start mysql
Or, if you don't want/need a background service you can just run:
  mysql.server start
==> Summary
?  /usr/local/Cellar/mysql/8.0.19: 286 files, 289.2MB

ここで上記出力を見ると、

We've installed your MySQL database without a root password

とあります。
HomebrewでMySQLをインストールするとrootパスワードは設定されないとのこと。
rootパスワード設定は次のステップで行います。

  • インストールされていれば、下記でバージョンが確認できます。
#バージョン確認
$ mysql --version

mysql  Ver 8.0.19 for osx10.15 on x86_64 (Homebrew)

MySQLの初期設定

rootパスワードが設定されていない等の問題があるため、セキュリティ設定を行います。(使えるのは使えるが)
いくつかYse or No を聞かれますが、特別なことがない限りYesでOKです。

ちなみに、セキュリティレベルを問われるところでの選択肢の内容は下記のとおりです。
勉強用に使うのであれば、LOWでOKです。

 ・LOW:8文字以上
 ・MEDIUM:8文字以上 + 数字・アルファベットの大文字と小文字・特殊文字を含む
 ・STRONG:8文字以上 + 数字・アルファベットの大文字と小文字・特殊文字を含む + 辞書ファイルでのチェック

#MySQLのセキュリティ設定
$ mysql_secure_installation

 Securing the MySQL server deployment.

Enter password for user root: 初期パスワードを入力する

The existing password for the user account root has expired. Please set a new password.

New password: 新しいパスワードを入力する

Re-enter new password: 再度同じ新しいパスワードを入力する

VALIDATE PASSWORD PLUGIN can be used to test passwords
and improve security. It checks the strength of password
and allows the users to set only those passwords which are
secure enough. Would you like to setup VALIDATE PASSWORD plugin?

Press y|Y for Yes, any other key for No: y

There are three levels of password validation policy:

LOW Length >= 8
MEDIUM Length >= 8, numeric, mixed case, and special characters
STRONG Length >= 8, numeric, mixed case, special characters and dictionary file

Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 0
Using existing password for root.

Estimated strength of the password: 100
Change the password for root ? ((Press y|Y for Yes, any other key for No) : y

New password: ポリシーに沿った新しいパスワードを入力

Re-enter new password: 再度新しいパスワードを入力する

Estimated strength of the password: 50
Do you wish to continue with the password provided?(Press y|Y for Yes, any other key for No) : y
By default, a MySQL installation has an anonymous user, a user account created for them. This is intended only for
testing, and to make the installation go a bit smoother.
You should remove them before moving into a production
environment.

Remove anonymous users? (Press y|Y for Yes, any other key for No) : y
Success.


Normally, root should only be allowed to connect from
'localhost'. This ensures that someone cannot guess at
the root password from the network.

Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y
Success.

By default, MySQL comes with a database named 'test' that
anyone can access. This is also intended only for testing,
and should be removed before moving into a production
environment.


Remove test database and access to it? (Press y|Y for Yes, any other key for No) : y Success.

- Removing privileges on test database...
Success.

Reloading the privilege tables will ensure that all changes
made so far will take effect immediately.

Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y
Success.

All done!

MySQLの起動

以下のコマンドで起動されます。

"MySQLの起動
$ mysql.server start

Starting MySQL
 SUCCESS! 

発生したエラーと対応

けっこう手こずったのでまとめておきます。
まとめてるけど、自分の場合のエラー原因はまだわかっていません。

MySQLが起動しない

  • きっかけは未だにわからないけれど、起動しなくなった。
$ mysql.server start

Starting MySQL
. ERROR! The server quit without updating PID file (/usr/local/var/mysql/マック名local.pid).

【トライ1】PIDファイルの作成

とりあえずPIDファイルを確認。
すると以下のようなPIDファイルが存在しないとのことだったので、PIDファイルの作成を実行。
しかしながら、起動できず。

# PIDファイルについて確認
$ ls -la /usr/local/var/mysql/XXXXXXXX.local.pid
# PIDがファイルが存在しないとのこと
ls: /usr/local/var/mysql/XXXXXXXX.local.pid: No such file or directory

#PIDファイルの作成
$ touch /usr/local/var/mysql/****.local.pid

【トライ2】権限をなおす

アクセス権限がなくてえらーが出るとの情報があったので、以下をトライ。
それでも起動できず。

$ sudo chown -R _mysql:_mysql /usr/local/var/mysql

【トライ3】MySQLの再インストール

もうこれしかないかなと思って、アンインストール→再インストール
調べると/usr/local/var/mysqlは消えないみたいなので、そちらは手動で削除。
が、これでも起動失敗。。。

#手動での削除
$ $ sudo rm -rf /usr/local/var/mysql
#MySQLのアンインストール
$ brew uninstall mysql
#MySQLの再インストール
$ brew install mysql

【トライ4】PC再起動→MySQL再起動

結局これで起動できた。
わからないままではちょっと、、なので原因調べます。

参考記事

以下の記事を参考にしました。ありがとうございます。
- https://reasonable-code.com/mysql-install/
- https://qiita.com/hkusu/items/cda3e8461e7a46ecf25d#%E6%89%8B%E9%A0%86
- https://teratail.com/questions/19778
- https://qiita.com/akizora001/items/4d63c75548520a70b845
- https://qiita.com/mogetarou/items/e34ca51d3756d55d7800

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Github Actions】Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)

事象

Github Actionsで突然Mysqlに接続できなくなった

Mysql2::Error::ConnectionError: Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)

原因

2020/3/12から、Github ActionsのUbuntuで
Mysqlが自動起動しなくなったため

https://github.blog/changelog/2020-02-21-github-actions-breaking-change-ubuntu-virtual-environments-will-no-longer-start-the-mysql-service-automatically/

解決策

workflowにMysqlを起動させるコマンドを追加

- run: |
    sudo /etc/init.d/mysql start

修正後ソース

name: Ruby

on:
  push:
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Set up Ruby 2.5
      uses: actions/setup-ruby@v1
      with:
        ruby-version: 2.5.x
    - name: Budle install
      run: |
        gem install bundler
        bundle install --jobs 4 --retry 3
    - name: Setup Database
      run: |
        cp config/database.yml.ci config/database.yml
        sudo /etc/init.d/mysql start # ここを追加
        bundle exec rake db:create
        bundle exec rake db:schema:load
      env:
        RAILS_ENV: test
    - name: test with Rake
      run: |
        bundle exec rake
      env:
        RAILS_ENV: test

最後に

公式ドキュメントちゃんと読みましょうという話

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MySQLのパーティショニングを活かすwhere句の書き方

実践ハイパフォーマンスMySQL 第3版という本(ちょっと古いけど)の中に、

MySQL がプルーニングを行うことができるのは、パーティショニング関数の列との比較に限ら れる。

引用元: 実践ハイパフォーマンスMySQL 第3版

という一節があったため確認してみた、という話です。

すべてのパーティションにアクセスしてしまうクエリ

例えば、create_onという列で年ごとにパーティションを切っているようなテーブルがあるとします。

create文
CREATE TABLE `test_table` (
 `pkey_col` int unsigned NOT NULL AUTO_INCREMENT,
 `some_col` int,
 `create_on` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
 PRIMARY KEY (`pkey_col`,`create_on`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
PARTITION BY RANGE COLUMNS(create_on)
(PARTITION p2018 VALUES LESS THAN ('2019-01-01') ENGINE = InnoDB,
 PARTITION p2019 VALUES LESS THAN ('2020-01-01') ENGINE = InnoDB,
 PARTITION p2020 VALUES LESS THAN ('2021-01-01') ENGINE = InnoDB);

このテーブルにパーティションで指定していない列で検索するクエリーを作ります。

最初のバージョンのSQL
SELECT
  some_col
FROM
  test_table
WHERE
  pkey_col = 123

実行計画はこのようになります。

+----+-------------+------------+-------------------+------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table      | partitions        | type | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+------------+-------------------+------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | test_table | p2018,p2019,p2020 | ref  | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
+----+-------------+------------+-------------------+------+---------------+---------+---------+-------+------+----------+-------+

partitionsのところにすべてのパーティションが並んでしまっていますね。

改善版

ここで、where句にパーティションで使用している列を指定してみます。

改善版のSQL
SELECT
  some_col
FROM
  test_table
WHERE
  pkey_col = 123 and
  create_on = '2020-03-21 0:19:00'

すると実行計画は次のようになります。

+----+-------------+------------+------------+-------+---------------+---------+---------+-------------+------+----------+-------+
| id | select_type | table      | partitions | type  | possible_keys | key     | key_len | ref         | rows | filtered | Extra |
+----+-------------+------------+------------+-------+---------------+---------+---------+-------------+------+----------+-------+
|  1 | SIMPLE      | test_table | p2020      | const | PRIMARY       | PRIMARY | 9       | const,const |    1 |   100.00 | NULL  |
+----+-------------+------------+------------+-------+---------------+---------+---------+-------------+------+----------+-------+

アクセスするパーティションが1つになりました。

アクセスするパーティションを限定することの効果

パーティションの限定はプルーニングと呼ばれており、クエリを早くする効果があります。

検索を制限することにより、テーブル内のすべてのパーティションをスキャンするよりも、一致する行を見つけるための時間と労力をはるかに少なくすることができます。 この不要なパーティションの「 切り取り 」は、 プルーニングとして知られています。 オプティマイザーがこのクエリの実行でパーティションプルーニングを利用できる場合、クエリの実行は、同じ列定義とデータを含むパーティション化されていないテーブルに対する同じクエリよりも桁違いに速くなります。

引用元: MySQL 8.0 Reference Manualを翻訳

このプルーニングを使うためには、WHERE句にパーティションで指定した列を指定する必要があります。

オプティマイザーは、 WHERE条件を次の2つのケースのいずれかに減らすことができる場合はいつでもプルーニングを実行できます。
partition_column = constant
partition_column IN ( constant1 , constant2 , ..., constantN )

引用元: 同上

まとめ

ということで、テーブルの設計や、where句の条件に気をつけないとパーティショニングの効果が薄れてしまうことがわかりました。

なお、MySQLはクエリでパーティションを明示的に指定することもできますので、下に記しておきます。

パーティションを指定する方法
SELECT
  some_col
FROM
  test_table PARTITION (p2020)
WHERE
  pkey_col = 123
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VSCユーザーがLaravelのデータベース接続設定でエラーが出た時、確認すべきこと

こんにちは!今回がエンジニア初学者がついうっかり詰まってしまった初歩的なエラーとその原因をご紹介していきたいと思います。

LaravelのDB接続設定エラー

入力した項目は合っているはずなのに、なぜかDB接続できない。

そんな時は、もう一度書き込んでいるファイルが正しいか確認してみてください!
もしかしたら.envファイルではなく.env.exampleに設定を書き込んでいるかもしれません。
DB接続設定を書き込むのは.envファイルです!

なぜこのエラーにハマったのか

原因は、普段使用しているVisualStudioCode(以下VSC)のクイックオープン(Cmd + P)でファイル検索をした時に、.envが引っ掛からず、ついうっかり検索結果に出ていた.env.exampleを私が.envだと誤認したからでした。

対応策

なぜ.envファイルが検索結果に出なかったのか。
それはVSCの設定で.ignoreファイルと.gitignoreファイルを検索対象から外すようにしていたからです。
検索対象に入れたい場合は、VSCの環境設定に入って検索ボックスからSearch:Use Ignore Filesを検索し、チェックを外しておくようにしましょう。
image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む