web-dev-qa-db-ja.com

INクエリにiBatis(myBatis)で注釈を使用する方法

MyBatisではアノテーションのみを使用します。私たちは本当にxmlを避けようとしています。 「IN」句を使用しようとしています。

@Select("SELECT * FROM blog WHERE id IN (#{ids})") 
List<Blog> selectBlogs(int[] ids); 

MyBatisは、intの配列を取り出して、それらを結果のクエリに入れることができないようです。 「ソフトに失敗する」ようで、結果は返されません。

XMLマッピングを使用してこれを達成できるように見えますが、それは本当に避けたいです。これに対する正しい注釈構文はありますか?

28
dirtyvagabond

これは、JDBCが準備したステートメントのニュアンスであり、MyBatisではないと思います。この問題を説明し、さまざまな解決策を提供するリンク here があります。残念ながら、これらの解決策はどれもアプリケーションに実行可能ではありませんが、 "IN"句に関する準備されたステートメントの制限を理解することは、良い読み物です。ソリューション(おそらく最適ではない)は、DB固有の側面にあります。たとえば、postgresqlでは、次のように使用できます。

"SELECT * FROM blog WHERE id=ANY(#{blogIds}::int[])"

「ANY」は「IN」と同じであり、「:: int []」は引数を整数の配列に型キャストします。ステートメントに渡される引数は次のようになります。

"{1,2,3,4}"
22
user199341

答えは この質問 で与えられたものと同じだと思います。次のようにして、注釈にmyBatis動的SQLを使用できます。

@Select({"<script>",
         "SELECT *", 
         "FROM blog",
         "WHERE id IN", 
           "<foreach item='item' index='index' collection='list'",
             "open='(' separator=',' close=')'>",
             "#{item}",
           "</foreach>",
         "</script>"}) 
List<Blog> selectBlogs(@Param("list") int[] ids);

<script>要素は、動的SQL解析と注釈の実行を可能にします。クエリ文字列の最初のコンテンツでなければなりません。その前には何もないはずです。空白もです。

さまざまなXMLスクリプトタグで使用できる変数は、通常のクエリと同じ命名規則に従うため、「param1」、「param2」など以外の名前を使用してメソッドの引数を参照する場合は、注意してください。各引数の前に@Paramアノテーションを付ける必要があります。

35
LordOfThePigs

このトピックに関するいくつかの調査がありました。

  1. mybatisの公式ソリューションの1つは、動的SQLを@Select("<script>...</script>")に配置することです。ただし、Javaアノテーションでxmlを記述することは非常に不快です。これについて考えてください@Select("<script>select name from sometable where id in <foreach collection=\"items\" item=\"item\" seperator=\",\" open=\"(\" close=\")\">${item}</script>")
  2. _@SelectProvider_は正常に動作します。しかし、読むのは少し複雑です。
  3. PreparedStatementでは整数のリストを設定できません。 pstm.setString(index, "1,2,3,4")を使用すると、SQLは次のようになりますselect name from sometable where id in ('1,2,3,4')。 Mysqlは文字_'1,2,3,4'_を数値_1_に変換します。
  4. FIND_IN_SETはmysqlインデックスでは機能しません。

Mybatis動的SQLメカニズムを調べてください。これはSqlNode.apply(DynamicContext)によって実装されています。ただし、_<script></script>_アノテーションなしの@Selectは、DynamicContextを介してパラメーターを渡しません。

も参照

  • _org.Apache.ibatis.scripting.xmltags.XMLLanguageDriver_
  • _org.Apache.ibatis.scripting.xmltags.DynamicSqlSource_
  • _org.Apache.ibatis.scripting.xmltags.RawSqlSource_

そう、

  • 解決策1:@SelectProviderを使用する
  • 解決策2:SQLを常にDynamicSqlSourceにコンパイルするLanguageDriverを拡張します。ただし、どこでも_\"_を記述する必要があります。
  • 解決策3:独自の文法をmybatisの文法に変換できるLanguageDriverを拡張します。
  • 解決策4:mybatis-velocityプロジェクトと同じように、テンプレートレンダラーを使用してSQLをコンパイルする独自のLanguageDriverを記述します。このようにして、groovyを統合することもできます。

私のプロジェクトはソリューション3を取り、これがコードです:

_public class MybatisExtendedLanguageDriver extends XMLLanguageDriver 
                                           implements LanguageDriver {
    private final Pattern inPattern = Pattern.compile("\\(#\\{(\\w+)\\}\\)");
    public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
        Matcher matcher = inPattern.matcher(script);
        if (matcher.find()) {
            script = matcher.replaceAll("(<foreach collection=\"$1\" item=\"__item\" separator=\",\" >#{__item}</foreach>)");
        }
        script = "<script>" + script + "</script>";
        return super.createSqlSource(configuration, script, parameterType);
    }
}
_

そして使用法:

_@Lang(MybatisExtendedLanguageDriver.class)
@Select("SELECT " + COLUMNS + " FROM sometable where id IN (#{ids})")
List<SomeItem> loadByIds(@Param("ids") List<Integer> ids);
_
15
yegong

私はコードに小さなトリックを作りました。

public class MyHandler implements TypeHandler {

public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
    Integer[] arrParam = (Integer[]) parameter;
    String inString = "";
    for(Integer element : arrParam){
      inString = "," + element;
    }
    inString = inString.substring(1);        
    ps.setString(i,inString);
}

そして、私はこのMyHandlerをSqlMapperで使用しました:

    @Select("select id from tmo where id_parent in (#{ids, typeHandler=ru.transsys.test.MyHandler})")
public List<Double> getSubObjects(@Param("ids") Integer[] ids) throws SQLException;

それは今動作します:)私はこれが誰かを助けることを望みます。

エフゲニー

6
pevgen

他のオプションは

    public class Test
    {
        @SuppressWarnings("unchecked")
        public static String getTestQuery(Map<String, Object> params)
        {

            List<String> idList = (List<String>) params.get("idList");

            StringBuilder sql = new StringBuilder();

            sql.append("SELECT * FROM blog WHERE id in (");
            for (String id : idList)
            {
                if (idList.indexOf(id) > 0)
                    sql.append(",");

                sql.append("'").append(id).append("'");
            }
            sql.append(")");

            return sql.toString();
        }

        public interface TestMapper
        {
            @SelectProvider(type = Test.class, method = "getTestQuery")
List<Blog> selectBlogs(@Param("idList") int[] ids);
        }
    }
3
Mohit Verma

Oracleでは、不明なリストサイズを処理するために、 Tom Kyteのトークナイザー のバリアントを使用します(IN句に対するOracleの1kの制限とそれを回避するために複数のINを実行することの悪化を想定)。これはvarchar2用ですが、数値に合わせて調整できます(または、 '1' = 1/shudderであることを知っているOracleに依存することもできます)。

MyBatisの呪文をパスまたは実行して、idsを文字列として取得し、それを使用するとします。

select @Select("SELECT * FROM blog WHERE id IN (select * from table(string_tokenizer(#{ids}))")

コード:

create or replace function string_tokenizer(p_string in varchar2, p_separator in varchar2 := ',') return sys.dbms_debug_vc2coll is
    return_value SYS.DBMS_DEBUG_VC2COLL;
    pattern varchar2(250);
begin
    pattern := '[^(''' || p_separator || ''')]+' ;

    select
        trim(regexp_substr(p_string, pattern, 1, level)) token
    bulk collect into
        return_value
    from
        dual
    where
        regexp_substr(p_string, pattern, 1, level) is not null
    connect by
        regexp_instr(p_string, pattern, 1, level) > 0;

    return return_value;
end string_tokenizer;
0
Llanfar

これを行うには、カスタムタイプハンドラーを使用できます。例えば:

_public class InClauseParams extends ArrayList<String> {
   //...
   // marker class for easier type handling, and avoid potential conflict with other list handlers
}
_

MyBatis設定に次のタイプハンドラーを登録します(またはアノテーションで指定します)。

_public class InClauseTypeHandler extends BaseTypeHandler<InClauseParams> {

    @Override
    public void setNonNullParameter(final PreparedStatement ps, final int i, final InClauseParams parameter, final JdbcType jdbcType) throws SQLException {

        // MySQL driver does not support this :/
        Array array = ps.getConnection().createArrayOf( "VARCHAR", parameter.toArray() );
        ps.setArray( i, array );
    }
    // other required methods omitted for brevity, just add a NOOP implementation
}
_

その後、このように使用できます

_@Select("SELECT * FROM foo WHERE id IN (#{list})"
List<Bar> select(@Param("list") InClauseParams params)
_

ただし、MySQLコネクタは準備されたステートメントのsetArray()をサポートしていないため、これはnotがMySQLで機能します。

MySQLの可能な回避策は、INの代わりに_FIND_IN_SET_を使用することです。

_@Select("SELECT * FROM foo WHERE FIND_IN_SET(id, #{list}) > 0")
List<Bar> select(@Param("list") InClauseParams params)
_

そして、あなたの型ハンドラは次のようになります:

_@Override
    public void setNonNullParameter(final PreparedStatement ps, final int i, final InClauseParams parameter, final JdbcType jdbcType) throws SQLException {

        // note: using Guava Joiner! 
        ps.setString( i, Joiner.on( ',' ).join( parameter ) );
    }
_

注:_FIND_IN_SET_のパフォーマンスがわからないので、重要な場合はテストしてください

0
sgdesmet

私のプロジェクトでは、すでにGoogle Guavaを使用しているので、簡単なショートカットを使用します。

public class ListTypeHandler implements TypeHandler {

    @Override
    public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, Joiner.on(",").join((Collection) parameter));
    }
}
0
user2665773