本文将从源代码的层面解析play自带的数据库管理插件。
先看程序的入口:
@Override
public void beforeInvocation() {
if(disabled || Play.mode.isProd()) {
return;
}
try {
checkEvolutionsState();
} catch (InvalidDatabaseRevision e) {
if ("mem".equals(Play.configuration.getProperty("db")) && listDatabaseEvolutions().peek().revision == 0) {
Logger.info("Automatically applying evolutions in in-memory database");
applyScript(true);
} else {
throw e;
}
}
}
如果当前是生产环境(prod)直接return,如果是开发环境,会进入checkEvolutionsState()
public synchronized static void checkEvolutionsState() {
if (getDatasource() != null && evolutionsDirectory.exists()) {
List<Evolution> evolutionScript = getEvolutionScript();
Connection connection = null;
try {
connection = getNewConnection();
ResultSet rs = connection.createStatement().executeQuery("select id, hash, apply_script, revert_script, state, last_problem from play_evolutions where state like 'applying_%'");
if (rs.next()) {
int revision = rs.getInt("id");
String state = rs.getString("state");
String hash = rs.getString("hash").substring(0, 7);
String script = "";
if (state.equals("applying_up")) {
script = rs.getString("apply_script");
} else {
script = rs.getString("revert_script");
}
script = "# --- Rev:" + revision + "," + (state.equals("applying_up") ? "Ups" : "Downs") + " - " + hash + "\n\n" + script;
String error = rs.getString("last_problem");
throw new InconsistentDatabase(script, error, revision);
}
} catch (SQLException e) {
throw new UnexpectedException(e);
} finally {
closeConnection(connection);
}
if (!evolutionScript.isEmpty()) {
throw new InvalidDatabaseRevision(toHumanReadableScript(evolutionScript));
}
}
}
仔细阅读源码会发现,第一句话的意思是:如果application.config中有db.xxx存在,且工程下有”db/evolutions“目录,程序将往下执行getEvolutionScript()方法,现在我们进入这个方法:
public synchronized static List<Evolution> getEvolutionScript() {
Stack<Evolution> app = listApplicationEvolutions();
Stack<Evolution> db = listDatabaseEvolutions();
List<Evolution> downs = new ArrayList<Evolution>();
List<Evolution> ups = new ArrayList<Evolution>();
// Apply non conflicting evolutions (ups and downs)
while (db.peek().revision != app.peek().revision) {
if (db.peek().revision > app.peek().revision) {
downs.add(db.pop());
} else {
ups.add(app.pop());
}
}
// Revert conflicting to fork node
while (db.peek().revision == app.peek().revision && !(db.peek().hash.equals(app.peek().hash))) {
downs.add(db.pop());
ups.add(app.pop());
}
// Ups need to be applied earlier first
Collections.reverse(ups);
List<Evolution> script = new ArrayList<Evolution>();
script.addAll(downs);
script.addAll(ups);
return script;
}
其中 listApplicationEvolutions()是读取”db/evolutions“目录下的n.sql文件,把相关信息排序以后放入一个Stack<Evolution>里面。
listDatabaseEvolutions()大致功能是创建”play_evolutions“表,如果这个表存在把表内容读入到Stack<Evolution>里面。
前面的stack取名为app,后面的stack取名为db,分别代表当前工程”db/evolutions“目录下的sql脚本信息和"play_evolutions"表信息
当aap的版本和db的版本不一致时的处理方式:
while (db.peek().revision != app.peek().revision) {
if (db.peek().revision > app.peek().revision) {
downs.add(db.pop());
} else {
ups.add(app.pop());
}
}
当版本一致但是hash不一致时的处理方式:
// Revert conflicting to fork node
while (db.peek().revision == app.peek().revision && !(db.peek().hash.equals(app.peek().hash))) {
downs.add(db.pop());
ups.add(app.pop());
}
当程序走完beforeInvocation()后会以抛出异常的形式把存入表"play_evolutions"的相关sql显示在页面,当按下“Apply_evolutions”的时候 会执行
@Override
public boolean rawInvocation(Request request, Response response) throws Exception {
// Mark an evolution as resolved
if (Play.mode.isDev() && request.method.equals("POST") && request.url.matches("^/@evolutions/force/[0-9]+$")) {
int revision = Integer.parseInt(request.url.substring(request.url.lastIndexOf("/") + 1));
resolve(revision);
new Redirect("/").apply(request, response);
return true;
}
// Apply the current evolution script
if (Play.mode.isDev() && request.method.equals("POST") && request.url.equals("/@evolutions/apply")) {
applyScript(true);
new Redirect("/").apply(request, response);
return true;
}
return super.rawInvocation(request, response);
}
这里包含了执行回滚sql的判断和执行正常sql的判断,进入applyScript(true)
public static synchronized boolean applyScript(boolean runScript) {
try {
Connection connection = getNewConnection();
int applying = -1;
try {
for (Evolution evolution : getEvolutionScript()) {
applying = evolution.revision;
// Insert into logs
if (evolution.applyUp) {
PreparedStatement ps = connection.prepareStatement("insert into play_evolutions values(?, ?, ?, ?, ?, ?, ?)");
ps.setInt(1, evolution.revision);
ps.setString(2, evolution.hash);
ps.setDate(3, new Date(System.currentTimeMillis()));
ps.setString(4, evolution.sql_up);
ps.setString(5, evolution.sql_down);
ps.setString(6, "applying_up");
ps.setString(7, "");
ps.execute();
} else {
execute("update play_evolutions set state = 'applying_down' where id = " + evolution.revision);
}
// Execute script
if (runScript) {
for (CharSequence sql : new SQLSplitter((evolution.applyUp ? evolution.sql_up : evolution.sql_down))) {
final String s = sql.toString().trim();
if (StringUtils.isEmpty(s)) {
continue;
}
execute(s);
}
}
// Insert into logs
if (evolution.applyUp) {
execute("update play_evolutions set state = 'applied' where id = " + evolution.revision);
} else {
execute("delete from play_evolutions where id = " + evolution.revision);
}
}
return true;
} catch (Exception e) {
String message = e.getMessage();
if (e instanceof SQLException) {
SQLException ex = (SQLException) e;
message += " [ERROR:" + ex.getErrorCode() + ", SQLSTATE:" + ex.getSQLState() + "]";
}
PreparedStatement ps = connection.prepareStatement("update play_evolutions set last_problem = ? where id = ?");
ps.setString(1, message);
ps.setInt(2, applying);
ps.execute();
closeConnection(connection);
Logger.error(e, "Can't apply evolution");
return false;
}
} catch (Exception e) {
throw new UnexpectedException(e);
}
}
可以看到这个方法的作用是更新“play_evolution”表,并执行app.up和db.down(如果app.up不存在).
执行完applyScript(true)后,url地址被转发回到当前请求页面,到这里大致的流程就走完了。
总结:我们可以看到,play每次读取的都是application.config中默认的db配置,即db.标注的数据库连接配置,所以同一时间只能做到更新到一台服务器,同时,回滚的方式为执行"# --- !down"标注下的sql,一旦误操作或者删除工程中配置的n.sql回滚以后写入的数据很难恢复。从程序设计角度来看play更鼓励我们在dev模式下使用这个插件(实际上,在生产环境中必须要执行play的脚本命令才能完成相关操作,具体参见“笔记(2)”)。同时程序做到了兼容mysql和oracle,且是线程安全的。