playframework 數據庫管理工具 筆記(3)

本文將從源代碼的層面解析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,且是線程安全的。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章