Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'INDEX `xxx` TO `yyy`' at line 1: ALTER TABLE `zzz` RENAME INDEX `xxx` TO `yyy`

とあるRuby製Webアプリの移管作業で発生したトラブルです。

 

データベースを作成するため、マイグレーションを実行したら、以下のようなエラーが発生しました。

 

Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'INDEX `xxx` TO `yyy`' at line 1: ALTER TABLE `zzz` RENAME INDEX `xxx` TO `yyy`

 

 

エラーの内容は、Rename Indexを実行したけど、SQLシンタックスエラーだよと。

 

Rename Indexは、MySQL5.7で追加された新機能ですが、当環境は、MySQL5.6を使用しています。

 

つまり、MySQL5.6には存在しないクエリを、ActiveRecordが発行したため、エラーになったわけです。

 

今までは動いていたと、聞いていたので、どういうことかと思い調べてみたら、ActiveRecordのバージョン5未満には、MySQLのバージョン判定に問題があることが、わかりました。

 

問題のコードは以下です。

 

/Project/Top/vendor/bundle/ruby/2.2.0/gems/activerecord-4.2.1/lib/active_record/connection_adapters/mysql2_adapter.rb

module ActiveRecord

  module ConnectionAdapters
    class Mysql2Adapter < AbstractMysqlAdapter

      def full_version
        @full_version ||= @connection.info[:version]
      end
    end
  end
end

 

これは、サーバ側のバージョンを取得せず、MySQLクライアントのバージョンを取得しているのです。

 

そこで、libmysql-devのバージョンを調べてみると、以前の環境は、5.5で、現在の環境は、5.7でした。

 

どうやら、libmysql-devのバージョンを取得し、MySQL5.7と判定して、MySQL5.7向けのクエリを作成してしまっているようです。

 

libmysql-devのバージョンをダウングレードができなかったので、問題のコードにパッチをあてて、サーバのバージョンを取得するように修正しました。

 

パッチは、こちらです。ネームスペース(module, class, method)を同一にして、上書きします。

 

/Project_Top/config/initializers/activerecord_patch.rb

module ActiveRecord
  module ConnectionAdapters
     class Mysql2Adapter < AbstractMysqlAdapter
       def full_version
         @full_version ||= @connection.server_info[:version]
       end
     end
   end
end 

 

パッチを読み込むようにします。上書きするので、読み込みのタイミングは最後になるように、`Padrino.after_load do`で、実行します。

 

config/boot.rb

Padrino.after_load do

  Dir.glob(Padrino.root + "/config/initializers/*.rb").each do |initlib|
    load initlib
  end

end

Powermock + SpringでJUnitをやってみた

Spring Bootはやり方があるらしいが、素のSpring Frameworkだったら、どうやるんだろう。
ぜんぜん、ぐぐっても出てこないので、はまりつつ、やってみました。

まず、アノテーション設定はこうします

  • @RunWithと@PowerMockRunnerDelegateで、JUnit + PowerMock + Spring の組みあわせる。
  • @PowerMockIgnoreしないとAESのエラー出る。
  • @PrepareForTestにMockにかかわるものを追加しておいた方がよいかも?
@ContextConfiguration(locations = {
        "classpath:PAHT/TO/application-context.xml"})
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class)
@PowerMockIgnore ("javax.crypto.*") 
@PrepareForTest({Target.class})
public class MockTest {
...

テスト対象のクラスTargetを@Autowiredつけて、ユニットテストクラスにDIする。
Targetクラスの中で、webServiceTemplateを使用して、外部システムのAPIを呼び出しているので、これをMockと差し替えたい。
だから、@MockをつけてMockしたいクラスとして宣言する。

public class MockTest {

     @Autowired
	private Target target;
	
     @Mock
     WebServiceTemplate webServiceTemplate;

Targetクラスの中で、webServiceTemplateは、実際は、`Response response = webServiceTemplate.marshalSendAndReceive(xxx, xxx)`というように使われています。
このメソッドをMockと差し替えて、意図した戻り値を返すように仕向けます。

	@Test
	public void test() throws Exception {
		
		Response response = new Response();
		response.setResultCode("XXXXX");
		PowerMockito.doReturn(response).when(webServiceTemplate).marshalSendAndReceive(anyString(), any());
		Whitebox.setInternalState(target, WebServiceTemplate.class, webServiceTemplate);

そして、targetを実行します。

		Result res = target.excute();
                Assert.assertEquals(XXX, res);
            }

エラーが出てしまった。
これがイケナイらしいです。

		Whitebox.setInternalState(target, WebServiceTemplate.class, webServiceTemplate);

なにをしているかというと、TargetクラスのPrivateフィールド変数webServiceTemplateを、
モック化したwebServiceTemplateで差し替えようとしています。

ところが、TargetクラスにPrivateフィールド変数webServiceTemplateが存在しないと言われました。

ん?そんなはずはない。

と思い、デバッガで、targetに、何が格納されているか見たら、Proxy〜みたいな見慣れないものが出ました。

それで、思いあたったのが、Spring AOP
実行時は、targetの中身は本来のインスタンスをラップしたインスタンスが格納されています。

困ったあげく、本来のインスタンスを取得できないか調べていたら、ありました。

targetが、AOPによって注入されたインスタンスだったら、そこから、本来のインスタンスをgetできるはず。
しかも、何個もAOPの処理を挟んでいるので、再帰的に剥がしていく必要がありました。

	static class AopUtils extends org.springframework.aop.support.AopUtils {
		
		@SuppressWarnings("unchecked")
		public static <T> T unwrapProxy(Object candidate) {
			
			try {
				if (isAopProxy(candidate) && (candidate instanceof Advised)) {
					return (T) unwrapProxy(((Advised) candidate).getTargetSource().getTarget());
				}
			}
			catch (Exception e) {
				throw new IllegalStateException("Failed to unwrap proxied object.", e);
			}

			// else
			return (T) candidate;
		}
	}

で、このようにしたら、うまくいきました。

		Whitebox.setInternalState(AopUtils.unwrapProxy(taget), WebServiceTemplate.class, webServiceTemplate);

jmockitのDeencapsulationがdeletedされてた

jmockitには、DeencapsulationというマジカルなUtilityクラスがあります。
通常は、アクセスできないフィールドやメソッドにアクセスすることができちゃう禁断のクラスです。
このクラスが、最新のバージョンで削除されていることが発覚しました。

ことの経緯としては、

1. Java 1.6からJava 1.8にVersion Upしたら
2. jmockitが動かない。内部で、JDKのVersionを判定しているようだ。
3. jmockitをVersion Upしたら
4. DeencapsulationがNoClassDef...

となって、まじか.. ぼくの大すきなDeencapsulationが・・・
とっても便利に使っていたのにどうしよぉおおおパキャマラド パキャマラド
パオパオパオパオ パオパオパオパオと考えていたら、
そうだ。似たものをつくって代用しちゃおうということになりました。

package util;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

import org.apache.commons.lang.reflect.FieldUtils;

/**
 * Reflection Utilities.
 * 
 *
 */
public class ReflectionUtils {
	
    /**
     * デフォルトコンストラクタの禁止.
     */
    private ReflectionUtils() {
    }

    /**
     * 対象のスタティックフィールド変数を指定された値で書き換えます.
     * 
     * @param cls 書き換えたいフィールド変数を持つクラス(スタティック変数の変更. static finalもOK. )
     * @param fieldName フィールド変数名
     * @param value 値
     * @throws NoSuchFieldException
     * @throws SecurityException
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     */
    public static void writeDeclaredStaticField(@SuppressWarnings("rawtypes") Class cls, String fieldName, Object value) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
        Field field = FieldUtils.getDeclaredField(cls, fieldName, true);
        setFinalStatic(field, value);
    }

    /**
     * 
     * Deencapsulation#setFieldの代用(インスタンス変数の変更。privateもOK.)
     * 
     * 対象インスタンスのフィールド変数を指定された値で書き換えます.
     * 
     * @param cls 書き換えたいフィールド変数を持つクラス
     * @param fieldName フィールド変数名
     * @param value 値
     * @throws IllegalAccessException
     */
    public static void writeField(Object target, String fieldName, Object value) throws IllegalAccessException {
    	FieldUtils.writeField(target, fieldName, value, true);
    }

    private static void setFinalStatic(Field field, Object newValue) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
          field.setAccessible(true);
          Field modifiersField = Field.class.getDeclaredField("modifiers");
          modifiersField.setAccessible(true);
          modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
          field.set(null, newValue);
       }
}


しばらく、javaから離れていたので、トレンドがよくわかりませんが
今、Mockライブラリといえば、PowerMockitoが主流なのでしょうかね。