Play 1.x框架學習之七:多數據庫切換與源碼修改 ( Databases Switch And Modify Source Code)

在單數據源(單個ip)下的多庫,可使用use xxdb 命令進行切換,可是若是多個數據源的狀況下,use命令就不行了。在play框架中,提供了多數據源多庫的切換。本文不提供徹底的例子,只提供部分的代碼,並且重點是在後面的修改源碼。 若是須要多ip多庫切換,就必須有一個主庫,保存全部分庫的信息。就如雲應用中,須要保存全部租戶的數據源與庫名,由於多是多個庫共用一個服務器,多個服務器構成集羣雲應用。 首先須要配置數據源,其中有主庫的db與分庫db_01: Conf/application.confjava

jpa.dialect=org.hibernate.dialect.MySQLDialect
db.url=jdbc:mysql://basedbip:3306/pop?autoReconnect=true&useUnicode=true&characterEncoding=utf-8
db.driver=com.mysql.jdbc.Driver
db.user=root
db.pass=root
db=pop

db_01.url=jdbc:mysql://anotherdbip:3306/pop?autoReconnect=true&useUnicode=true&characterEncoding=utf-8
db_01.driver=com.mysql.jdbc.Driver
db_01.user=root
db_01.pass=root
db_01=pop01

切庫的方法以下: Util/DB.javamysql

package util;

import play.db.jpa.JPA;

public class DB {
    public static void changeDB(String dbconfigname, String dbname){
        try {
            JPA.setCurrentConfigName(dbconfigname);
            if (JPA.em().getTransaction().isActive() == false) {
                JPA.em().getTransaction().begin();
            }
            JPA.em().createNativeQuery("use "+dbname).executeUpdate();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

完成上述配置後,能夠進行自由切庫,可是若是你實際運行中,卻出現一個重大問題。就是在controller層涉及數據封裝,如:修改用戶:redis

public static void update(@Required @Valid User user){
    user.save();
    renderText(user.id);
}

在封裝user對象參數的時候,play1.x框架的自動查詢數據庫,這個處理是在controller方法以前,因此根本沒法切庫!由於主庫與分庫表與數據不一樣,因此會報錯!sql

play.exceptions.UnexpectedException: Unexpected Error
    at play.data.validation.ValidationPlugin.beforeActionInvocation(ValidationPlugin.java:59)
    at play.plugins.PluginCollection.beforeActionInvocation(PluginCollection.java:518)
    at play.mvc.ActionInvoker.invoke(ActionInvoker.java:131)
    at Invocation.HTTP Request(Play!)
Caused by: play.exceptions.UnexpectedException: Unexpected Error
    at play.db.jpa.JPAPlugin.bind(JPAPlugin.java:67)
    at play.plugins.PluginCollection.bind(PluginCollection.java:468)
    at play.data.binding.Binder.bind(Binder.java:309)
    at play.mvc.ActionInvoker.getActionMethodArgs(ActionInvoker.java:621)
    at play.data.validation.ValidationPlugin$Validator.validateAction(ValidationPlugin.java:95)
    at play.data.validation.ValidationPlugin.beforeActionInvocation(ValidationPlugin.java:51)
    ... 3 more
Caused by: javax.persistence.PersistenceException: org.hibernate.exception.SQLGrammarException: could not execute query
    at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1214)
    at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1147)
    at org.hibernate.ejb.QueryImpl.getSingleResult(QueryImpl.java:307)
    at play.db.jpa.JPAPlugin.bind(JPAPlugin.java:62)
    ... 8 more
Caused by: org.hibernate.exception.SQLGrammarException: could not execute query
    at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:92)
    at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
    at org.hibernate.loader.Loader.doList(Loader.java:2536)
    at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2276)
    at org.hibernate.loader.Loader.list(Loader.java:2271)
    at org.hibernate.loader.hql.QueryLoader.list(QueryLoader.java:452)
    at org.hibernate.hql.ast.QueryTranslatorImpl.list(QueryTranslatorImpl.java:363)
    at org.hibernate.engine.query.HQLQueryPlan.performList(HQLQueryPlan.java:196)
    at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1268)
    at org.hibernate.impl.QueryImpl.list(QueryImpl.java:102)
    at org.hibernate.ejb.QueryImpl.getSingleResult(QueryImpl.java:274)
    ... 9 more
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table ´pop.user´ doesn´t exist
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:407)
    at com.mysql.jdbc.Util.getInstance(Util.java:382)
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1052)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3593)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3525)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1986)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2140)
    at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2626)
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:2111)
    at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:2273)
    at org.hibernate.jdbc.AbstractBatcher.getResultSet(AbstractBatcher.java:208)
    at org.hibernate.loader.Loader.getResultSet(Loader.java:1953)
    at org.hibernate.loader.Loader.doQuery(Loader.java:802)
    at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:274)
    at org.hibernate.loader.Loader.doList(Loader.java:2533)
    ... 17 more

看到錯誤棧最後一條信息,說表不存在! 可能你想在父類控制器controller利用@before標籤,統一在方法調用前,進行攔截配置。可是結果會讓你失望!錯誤會依舊! 這時候咱們須要根據報錯信息,深刻play框架源碼。 從caused by中看到JPAPlugin.java,但是已經用到JPA查詢數據庫了。在第二個caused by中 JPAPlugin.java:62 處能夠看到jpaplugin的bind方法以下:數據庫

@Override
@SuppressWarnings("unchecked")
public Object bind(String name, Class clazz, java.lang.reflect.Type type, Annotation[] annotations, Map params) {
    // TODO need to be more generic in order to work with JPASupport
    if (JPABase.class.isAssignableFrom(clazz)) {
        String keyName = Model.Manager.factoryFor(clazz).keyName();
        String idKey = name + "." + keyName;
        if (params.containsKey(idKey) && params.get(idKey).length > 0 && params.get(idKey)[0] != null && params.get(idKey)[0].trim().length() > 0) {
            String id = params.get(idKey)[0];
            try {
                Query query = JPA.em().createQuery("from " + clazz.getName() + " o where o." + keyName + " = ?");
                query.setParameter(1, play.data.binding.Binder.directBind(name, annotations, id + "", Model.Manager.factoryFor(clazz).keyType()));
                Object o = query.getSingleResult();
                return GenericModel.edit(o, name, params, annotations);
            } catch (NoResultException e) {
                // ok
            } catch (Exception e) {
                throw new UnexpectedException(e);
            }
        }
        return GenericModel.create(clazz, name, params, annotations);
    }
    return super.bind(name, clazz, type, annotations, params);
}

一目瞭然,方法中第70行服務器

Query query = JPA.em().createQuery("from " + clazz.getName() + " o where o." + keyName + " = ?");

是在根據主鍵查詢數據表中數據,可是這個時候尚未有切到相應的分庫,因此是沒有要查詢表的!(就算主庫中相應的表,也沒有相應的數據)。 爲何說沒有切庫呢?不是用@before處理了嗎?咱們繼續追尋錯誤信息到ActionInvoker.java 中的invoke方法,代碼太多,只貼出錯誤提示的131行附近session

ControllerInstrumentation.stopActionCall();
        Play.pluginCollection.beforeActionInvocation(actionMethod);

        // Monitoring
         monitor = MonitorFactory.start(request.action + "()");

        // 3. Invoke the action
        try {
            // @Before
            handleBefores(request);

            // Action

            Result actionResult = null;
            String cacheKey = null;

在此處mvc

Play.pluginCollection.beforeActionInvocation(actionMethod);

play框架處理了一系列插件調用前的準備工做,包括redisplugin/jpaplugin/validationplugin/wsplugin等等,其中的validationplugin就是處理全部有校驗註解的實體類,同時也包括打了@id註解的主鍵而處理@before的方法在後面的app

handleBefores(request);

因此說 用befores也是無用的! 這裏一個可行的方案是修改play源碼,仿照befores註解的處理,在play.mvc下添加一個BeforeValidation註解,在校驗以前進行切庫!框架

play.mvc.BeforeValidation.java
package play.mvc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Mark this method as @BeforeValidation interceptor
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BeforeValidation { 
    
    /**
     * Does not intercept these actions
     */
    String[] unless() default {};
    String[] only() default {};

    /**
     * Interceptor priority (0 is high priority)
     */
    int priority() default 0; 
}

同時在ActionInvoker中添加方法handleBeforeValidations play.mvc.ActionInvoker.java

private static void handleBefores(Http.Request request) throws Exception {
        List befores = Java.findAllAnnotatedMethods(Controller.getControllerClass(), Before.class);
        Collections.sort(befores, new Comparator() {

            public int compare(Method m1, Method m2) {
                Before before1 = m1.getAnnotation(Before.class);
                Before before2 = m2.getAnnotation(Before.class);
                return before1.priority() - before2.priority();
            }
        });
        ControllerInstrumentation.stopActionCall();
        for (Method before : befores) {
            String[] unless = before.getAnnotation(Before.class).unless();
            String[] only = before.getAnnotation(Before.class).only();
            boolean skip = false;
            for (String un : only) {
                if (!un.contains(".")) {
                    un = before.getDeclaringClass().getName().substring(12).replace("$", "") + "." + un;
                }
                if (un.equals(request.action)) {
                    skip = false;
                    break;
                } else {
                    skip = true;
                }
            }
            for (String un : unless) {
                if (!un.contains(".")) {
                    un = before.getDeclaringClass().getName().substring(12).replace("$", "") + "." + un;
                }
                if (un.equals(request.action)) {
                    skip = true;
                    break;
                }
            }
            if (!skip) {
                before.setAccessible(true);
                inferResult(invokeControllerMethod(before));
            }
        }
    }

再將handleBeforeValidations處理方法,放到 Play.pluginCollection.beforeActionInvocation前面

ControllerInstrumentation.stopActionCall();
    
    //@BeforeValidation
    handleBeforeValidations(request);
    
    Play.pluginCollection.beforeActionInvocation(actionMethod);

    // Monitoring
    monitor = MonitorFactory.start(request.action + "()");

    // 3. Invoke the action
    try {
        // @Before
        handleBefores(request);

        // Action

        Result actionResult = null;
        String cacheKey = null;

這樣一來,就能夠經過BeforeValidation註解在application中添加相應的切庫邏輯。就能夠解決Jpa封裝對象參數報錯問題。 另外還有切庫依據,能夠將相應的數據源等數據存放一個對象,用sessionid作key存放在redis中,每次請求過來就進行切庫,順便檢測是否登錄。

相關文章
相關標籤/搜索