You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让Play框架应用执行CRUD API前运行SQL脚本?Evolutions失效排查

嘿,我来帮你搞定这两个问题——先是Play Evolutions没自动执行SQL脚本的坑,再是如何在CRUD API跑起来前执行自定义SQL脚本。

一、解决Play Evolutions未自动执行SQL脚本的问题

从你的配置和描述来看,主要有几个可能的原因,咱们一个个排查:

1. 确认你加对了依赖

要让Evolutions和Slick配合工作,你必须在build.sbt里引入play-slick-evolutions依赖,不然Evolutions根本不会和Slick的数据源联动。把这段加到你的依赖列表里:

libraryDependencies ++= Seq(
  guice,
  "mysql" % "mysql-connector-java" % "8.0.33", // 对应你的MySQL版本
  "com.typesafe.play" %% "play-slick" % "5.0.0", // 对应你的Play版本
  "com.typesafe.play" %% "play-slick-evolutions" % "5.0.0"
)

2. 修正Slick数据源配置,和Play的数据源对齐

你当前的Slick配置是直接创建独立数据源,和Evolutions监听的Play默认数据源不是同一个,导致脚本执行了也没作用在Slick连接的库上。修改application.conf里的Slick配置:

slick.dbs.default.driver = "slick.jdbc.MySQLProfile$"
# 让Slick使用Play的HikariCP数据源,这样Evolutions能作用于它
slick.dbs.default.db.dataSourceClass = "play.api.db.HikariCPDataSource"
slick.dbs.default.db.properties.driver = "com.mysql.cj.jdbc.Driver"
slick.dbs.default.db.properties.url = "jdbc:mysql://localhost:3306/playscalaslickexample?useSSL=false&serverTimezone=UTC"
slick.dbs.default.db.properties.user = "你的MySQL用户名"
slick.dbs.default.db.properties.password = "你的MySQL密码"

注意:先手动在MySQL里创建playscalaslickexample数据库——Evolutions只会帮你建表,不会帮你建库!

3. 检查Evolutions脚本的格式和路径

  • 路径必须是conf/evolutions/default/1.sql(因为你的数据源名称是default,不是数据库名playscalaslickexample)
  • 脚本必须符合Evolutions的格式,开头要有# --- !Ups(执行的升级脚本)和# --- !Downs(回滚脚本),比如:
# --- !Ups
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL
);

# --- !Ups
INSERT INTO users(name) VALUES('测试用户');

# --- !Downs
DROP TABLE users;

如果没有这个格式,Evolutions会忽略这个脚本。

4. 确认Evolutions配置生效

你的现有Evolutions配置是对的,保持不变就行:

play.evolutions.enabled=true
play.evolutions.autoApply=true
play.evolutions.db.default.enabled = true

做完这些,重启应用,Evolutions应该会自动执行1.sql里的脚本了。

二、在执行控制器CRUD API方法前运行SQL脚本

如果需要在每次CRUD接口调用前执行特定SQL(比如记录审计日志、设置会话变量),有几种灵活的方式:

1. 自定义Action组合器(推荐,针对单个/部分接口)

创建一个可复用的Action包装器,在目标接口逻辑前执行SQL:

import play.api.mvc._
import slick.jdbc.MySQLProfile.api._
import scala.concurrent.ExecutionContext

// 注入Database来执行SQL
class PreRunSqlActions(db: Database)(implicit ec: ExecutionContext) {
  def withPreRunSql(action: Action[AnyContent]): Action[AnyContent] = Action.async { request =>
    // 这里写你要执行的SQL,比如记录请求日志
    val sqlTask = sql"INSERT INTO audit_log (path, request_time) VALUES (${request.path}, NOW())".asUpdate
    db.run(sqlTask).flatMap { _ =>
      // 执行完SQL再调用原接口逻辑
      action(request)
    }
  }
}

然后在控制器里用这个组合器包装你的CRUD接口:

class UserController @Inject()(
  val controllerComponents: ControllerComponents,
  db: Database,
  preRunSqlActions: PreRunSqlActions
)(implicit ec: ExecutionContext) extends BaseController {

  // 用自定义Action包装,执行CRUD前先跑SQL
  def getUsers() = preRunSqlActions.withPreRunSql {
    Action.async {
      db.run(Users.result).map(users => Ok(Json.toJson(users)))
    }
  }
}

2. 使用全局Filter(针对所有符合条件的接口)

如果要给所有CRUD接口统一加前置SQL,用Play的Filter拦截请求:

import play.api.mvc._
import slick.jdbc.MySQLProfile.api._
import scala.concurrent.{ExecutionContext, Future}

class PreRunSqlFilter(db: Database)(implicit ec: ExecutionContext) extends Filter {
  override def apply(nextFilter: RequestHeader => Future[Result])(requestHeader: RequestHeader): Future[Result] = {
    // 只对/api开头的CRUD接口生效,可根据自己的路由调整
    if (requestHeader.path.startsWith("/api")) {
      val sqlTask = sql"SET @current_request_id = '${requestHeader.id.toString}'".asUpdate
      db.run(sqlTask).flatMap { _ =>
        nextFilter(requestHeader)
      }
    } else {
      // 非CRUD接口直接放行
      nextFilter(requestHeader)
    }
  }
}

然后在Module.scala里绑定这个Filter:

import play.api.mvc.EssentialFilter
import com.google.inject.AbstractModule

class Module extends AbstractModule {
  override def configure(): Unit = {
    bind(classOf[EssentialFilter]).to(classOf[PreRunSqlFilter])
  }
}

3. 数据库会话级钩子(针对所有数据库操作)

如果要在每次获取数据库连接时执行SQL(比如设置SQL模式),可以扩展Slick的Database:

import slick.jdbc.MySQLProfile.api._
import scala.concurrent.ExecutionContext

object CustomDb {
  def withPreRunScript(db: Database)(implicit ec: ExecutionContext): Database = {
    Database.forDataSource(db.dataSource, None) { connection =>
      // 每次获取连接都执行这个SQL
      val stmt = connection.createStatement()
      stmt.execute("SET sql_mode = 'STRICT_TRANS_TABLES'")
      stmt.close()
      connection
    }
  }
}

之后在应用中用这个自定义的Database来执行操作即可。


内容的提问来源于stack exchange,提问作者Shashi Shekhar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:41:32