[ハンドティア MyBatis ソースコード] MyBatis マッピング システム

マッピングツールメタオブジェクト

いわゆるマッピングとは、結果セット内の列が JAVA Bean プロパティに入力されることを意味します。これにはリフレクションを使用する必要があり、Bean の属性は通常の属性、オブジェクト、コレクション、マップなどさまざまです。Bean プロパティをより便利に操作するために、MyBatis は MetaObject ツール クラスを提供します。このクラスは、データベース内のテーブルのフィールドを Java オブジェクトのプロパティにマップするために使用されます。Java オブジェクトのプロパティを取得、設定、判断するためのいくつかのメソッドを提供します。

基本的なスキル

その具体的な機能は次のとおりです。

  • 検索属性: 大文字と小文字を無視せず、キャメルケースをサポートし、「blog.comment.user_name」などのサブ属性をサポートします。
    ここに画像の説明を挿入

  • 属性を取得する

    • .サブプロパティ「user.name」の取得に基づいています
    • インデックスに基づいてリスト値「users[1].id」を取得します
    • キーに基づいてマップ値「user[name]」を取得します
  • プロパティを設定します。

    • 設定可能なサブプロパティ値
    • サブプロパティの自動作成をサポートします (null パラメーター コンストラクターが必要であり、コレクションにすることはできません)
      ここに画像の説明を挿入
      ここに画像の説明を挿入
      ここに画像の説明を挿入

主な方法は次のとおりです。

  • getGetter(String name): プロパティを取得するためのゲッターメソッド
  • getSetter(String name): プロパティのセッターメソッドを取得します。
  • getGetterType(String name): 属性のタイプを取得します。
  • hasGetter(String name): getterメソッドがあるかどうかを判定します。
  • hasSetter(String name): setterメソッドがあるかどうかを判定します。
  • getValue(文字列名): 属性の値を取得します。
  • setValue(文字列名, オブジェクト値): プロパティの値を設定します。
  • findProperty(String name, boolean useCamelCaseMapping): プロパティを検索するときに、キャメルケースの名前付けを使用するかどうかを指定できます (戻り値の型は String です)。
public class User {
    
    
    private int id;
    private String name;
    
    public int getId() {
    
    
        return id;
    }
    public void setId(int id) {
    
    
        this.id = id; 
    }
    public String getName() {
    
    
        return name;
    }
    public void setName(String name) {
    
    
        this.name = name;
    }
}

MetaObject metaObject = MetaObject.forObject(user);
// 获取 getter 方法  
Method idGetter = metaObject.getGetter("id");  
// 获取 setter 方法
Method nameSetter = metaObject.getSetter("name");
// 获取 name 属性的值
String name = (String) metaObject.getValue("name");
// 设置 id 属性的值 
metaObject.setValue("id", 1);

基礎構造

上記の機能を実現するために、MetaObject は BeanWrapper、MetaClass、および Reflector に依存してきました。これら 4 つのオブジェクトは次のように関連しています。

ここに画像の説明を挿入

  • BeanWrapper: 関数は MeataObject に似ていますが、違いは次のとおりです。BeanWrapper は、単一の現在のオブジェクト プロパティに対してのみ操作できます。、サブプロパティを操作できません。
    • したがって、Metaobject が属性の属性を検索するときは、属性を徐々に検索し、次に属性の属性を検索します。このプロセスは BeanWrapper を通じて検索され、Metaobject は式の解析に使用されます。
    • BeanWrapper の最下層はリフレクションに基づく必要があります。ここでは MetaClass を使用します。
  • MetaClass: クラスのリフレクション機能をサポートし、属性の属性も含めてクラス全体の属性を取得できます 基礎層はReflectorをベースとしています。
  • リフレクター: クラスのリフレクション機能がサポートされ、現在のクラスの属性のみがサポートされます。

属性値を取得するプロセス

オブジェクトの構造は次のとおりです。

ここに画像の説明を挿入
ブログの最初のコメント投稿者の名前を取得します。get 式は次のとおりです。

"comments[0].user.name"

MetaObjbt の分析と取得のプロセスは次のとおりです。

ここに画像の説明を挿入

このプロセスから、MetaObject は再帰を使用して式をレイヤーごとに解析し、サブ属性がなくなるまで実際の取得作業を BeanWrapper に渡さず、BeanWrapper の最下位レイヤーがリフレクションに基づいて属性値を取得することがわかります。

コレクション インデックスが関係する場合は、最初にコレクションを取得し、次にインデックスに基づいてコレクション内の値を取得します。作業のこの部分は BaseWrapper に引き渡されます。

プロセス内のメソッドの説明:

  • MetaObject.getValue(): 属性の値を取得するには、まず属性名「comments[0].user.name」に基づいて PropertyTokenizer に解析し、属性内の「.」に基づいてサブ属性値であるかどうかを判断し、サブ属性値である場合は getValue() を再帰的に呼び出してサブ属性オブジェクトを取得します。次に、getValue() を再帰的に呼び出して、サブプロパティの下のプロパティを取得します。姓属性まで。

  • MetaObject.setValue(): このプロセスは getValue() に似ていますが、サブプロパティが存在しない場合にサブプロパティの作成を試みる点が異なります。

ソースコードを結合しましょう:

MetaObject の式の解析は、実際には PropertyTokenizer と呼ばれるプロパティ トークナイザーに依存していることがわかります。

ここに画像の説明を挿入
次に、このトークナイザーには次のメソッドがあり、子に従ってトークナイザーを生成し、レイヤーごとの解析の効果を実現します。
ここに画像の説明を挿入
ここに画像の説明を挿入

ここでコレクションは BeanWrapper の get メソッドを使用します。

ここに画像の説明を挿入

ここに画像の説明を挿入

ResultMap 結果セットのマッピング

マッピングとは、返された ResultSet 列と Java Bean プロパティ間の対応関係を指します。マッピングの記述はResultMappingを通じて行われ、ResultMapで全体としてパッケージ化されます。

ここに画像の説明を挿入

手動マッピング

マッピング設定
ここに画像の説明を挿入
ここに画像の説明を挿入

ResultMap には、特定の JAVA 属性と列のマッピングを表す複数の ResultMapping が含まれており、その主な値は次のとおりです。

ここに画像の説明を挿入
ResultMapping には次のようなさまざまなマニフェストがあります。

  • コンストラクタ: 構築パラメータフィールド
  • id: IDフィールド
  • 結果: 共通構造セットフィールド
  • 関連:1対1の関連フィールド(オブジェクトを関連付ける場合)
  • コレクション: 1 対多のコレクション関連付けフィールド

ここでコンストラクターについて説明します。
resultMap のコンストラクター要素は、結果セットのマッピング プロセス中にクラスのコンストラクターを呼び出すために使用されます。
MyBatis が結果オブジェクトを構築するとき、デフォルトではデフォルトのコンストラクターを通じてターゲット クラスをインスタンス化し、次に setter メソッドを通じてそれに値を割り当てます。
ただし、クラスにデフォルトのコンストラクターがない場合や、コンストラクターを介してデフォルト値を設定する必要がある場合、コンストラクター要素は非常に便利です。
resultMap のコンストラクターを設定してターゲット クラスの構築メソッドを指定することができ、MyBatis は結果オブジェクトを構築するときにこの構築メソッドを呼び出します。
コンストラクター構成の構文は次のとおりです。

<constructor> 
 <idArgumnet index="0" javaType="int"/>
 <arg index="1" javaType="String"/> 
</constructor>
  • idArgumnet: コンストラクターの id に使用されるパラメーターを指定します。
  • arg: コンストラクター内の通常のパラメーターを指定します。
  • Index: コンストラクターのパラメーター リスト内のパラメーターの位置を指定します。
  • javaType: 指定されたパラメータの Java タイプ。

次に、MyBatis が結果オブジェクトを構築するときに、一致する構築メソッドを見つけて構築メソッドを呼び出し、idArgumnet で指定された id パラメータと arg で指定された各パラメータを通じてオブジェクトをインスタンス化します。

たとえば、ユーザー クラス User があります。

public class User {
     
     
 private int id;
 private String name;
 
 public User(int id, String name) {
     
     
   this.id = id;
   this.name = name;
 }
}

resultMap は次のように構成できます。

<resultMap id="userMap" type="User">
 <constructor>
   <idArg index="0" javaType="int"/>
   <arg index="1" javaType="String"/>
 </constructor> 
</resultMap> 

次に、MyBatis が結果セットを処理して User オブジェクトにマップすると、User(int, String) 構築メソッドが見つかり、それを呼び出してUser(1, "Tom")User オブジェクトをインスタンス化します。
コンストラクターが設定されていない場合、MyBatis はデフォルトで引数なしのコンストラクターを呼び出してインスタンスUser()を作成し、setter メソッドを通じて ID と名前に値を割り当てます。

User user = new User(); 
user.setId(1);
user.setName("Tom");

コンストラクターを使用するとパフォーマンスに一定の利点があり、引数のない構築方法の状況を回避できることがわかります。
したがって、constructor 要素は、resultMap の結果オブジェクト構築プロセスに豊富な選択肢を提供し、必要に応じて、デフォルトの構築 + セッター メソッドまたは構築メソッドを使用して結果オブジェクトをインスタンス化することを選択できます。これも、MyBatis の柔軟性の力を反映しています。

ここに画像の説明を挿入

自動マッピング

現在の列名と属性名が同じ場合に自動マッピングを使用できます。

ここに画像の説明を挿入
自動マッピング条件

  • 列名と属性名が同時に存在します(大文字、キャメルケース、アンダースコアを省略しないでください
  • 現在の列には手動で設定されたマッピングがありません
  • プロパティクラスが存在します TypeHandler
  • 自動マッピングを有効にする (デフォルトで有効)

自動マッピングは、複雑なマッピングが含まれる場合、左側の 3 つのケースでのみ確立できます。
ここに画像の説明を挿入

ネストされたサブクエリ

まず、ネストされたサブクエリの起源について話しましょう。
関連付け要素は、「has a」タイプの関係を処理します。たとえば、この例では、ブログにユーザーがいます。アソシエーション結果のマッピングは、他のタイプのマッピングとほぼ同じように機能します。ターゲットのプロパティ名とプロパティの javaType を指定する必要があります (多くの場合、MyBatis はそれ自体を推論できます)。必要に応じて JDBC タイプを設定することもでき、結果値を取得するプロセスをオーバーライドする場合はタイプ ハンドラーを設定することもできます。
関連付けとの違いは、関連付けをロードする方法を MyBatis に指示する必要があることです。MyBatis には関連付けをロードする 2 つの異なる方法があります (もちろん Collection も使用できます)。

  • 嵌套 Select 查询: 別の SQL マッピング ステートメントを実行して、目的の複合タイプをロードします。
    ここに画像の説明を挿入

それはとても簡単です。2 つの選択クエリがあります。1 つはブログ (Blog) をロードするもの、もう 1 つは作成者 (Author) をロードするもので、ブログの結果マップには、その author プロパティが selectAuthor ステートメントを使用してロードされる必要があることが記述されています。
他のすべてのプロパティは、列名がプロパティ名と一致する限り、自動的にロードされます。
このアプローチは単純ですが、大規模なデータセットや大規模なテーブルではうまく機能しません。この問題は「N+1 クエリ問題」として知られています。簡単に言うと、N+1 クエリの問題は次のようになります。

  • 単一の SQL ステートメントを実行して、結果のリスト (つまり、「+1」) を取得します。
  • リストによって返された各レコードに対して、選択クエリを実行して、各レコードの詳細 (つまり、「N」) をロードします。

この問題により、数百または数千の SQL ステートメントが実行される可能性があります。場合によっては、そのような結果を望まないこともあります。
幸いなことに、MyBatis はそのようなクエリを遅延ロードできるため、多数のステートメントを同時に実行するオーバーヘッドを分散できます。ただし、レコードのリストをロードした後、すぐにリストを反復処理してネストされたデータをフェッチすると、すべての遅延ロード クエリがトリガーされ、パフォーマンスが非常に低下する可能性があります。

  • 嵌套结果映射: ネストされた結果マップを使用して、結合結果の繰り返しのサブセットを処理します。
    ここに画像の説明を挿入

ただし、多くの場合、オブジェクト構造はツリー レベルで表示されます。つまり、オブジェクトにはオブジェクトが含まれます。サブオブジェクトのプロパティはサブクエリを通じて取得できます。

ここに画像の説明を挿入
ブログ内の属性を順番に解析する場合、まず共通の属性を解析して設定します。また、複合オブジェクトに解析する場合は、一対のサブクエリをトリガーします。

ここに画像の説明を挿入

循環依存関係

次の図に示すように、2 つのオブジェクトは相互に参照します。つまり、循環参照です。

ここに画像の説明を挿入
対応する ResultMap は次のとおりです。
ここに画像の説明を挿入
この状況では解析の無限ループが発生しますか? 答えはいいえだ。DefaultResultSetHandler は、複合マップを解析する前に、コンテキスト内に現在の解析オブジェクト (resultMapId をキーとして使用) を設定します。サブプロパティ マッピングが親マッピング ID を参照している場合、親オブジェクトを解析せずに直接取得できます。具体的なプロセスは次のとおりです。
ここに画像の説明を挿入
具体的なコード:
ここに画像の説明を挿入

遅延読み込み

遅延読み込みは、オブジェクトのプロパティを解析する際の多数のネストされたサブクエリの同時実行性を向上させることです。遅延ロードを設定すると、指定された属性が使用された場合にのみロードされるため、SQL リクエストが分散されます。

<resultMap id="blogMap" type="blog" autoMapping="true">
    <id column="id" property="id"></id>
    <association property="comments" column="id" select="selectCommentsByBlog" fetchType="lazy"/>
</resultMap>

遅延ロードは、ネストされたサブクエリで fetchType="lazy" を指定することで設定できます。getComments が呼び出されるまで、実際にはロードされません。さらに、「equals」、「clone」、「hashCode」、「toString」の呼び出しにより、現在のオブジェクトの未実行の遅延ロードがすべてトリガーされます。グローバルパラメータaggressiveLazyLoading=trueを設定すると、オブジェクトの任意のメソッドを呼び出してすべての遅延読み込みをトリガーするように指定することもできます。
ここに画像の説明を挿入

オーバーライドとシリアル化を設定する

setXXX メソッドを呼び出して属性を手動で設定すると、対応する属性の遅延読み込みが削除され、手動で設定した値は上書きされません。

オブジェクトがシリアル化および逆シリアル化されると、遅延読み込みはデフォルトではサポートされなくなります。ただし、グローバルパラメータにconfigurationFactoryクラスを設定し、Javaネイティブシリアル化を使用する場合は、遅延ロードは正常に実行できます。原理としては、遅延読み込みに必要なパラメータと設定をまとめてシリアライズし、デシリアライズ後にconfigurationFactoryを通じて実行環境を構築するための設定を取得します。

configurationFactory は、getConfiguration 静的メソッドを含むクラスです。

public static class ConfigurationFactory {
    
    
        public static Configuration getConfiguration() {
    
    
        return configuration;
    }
}

原理

MyBatis には、遅延読み込みを構成するための 2 つのタグ (関連付けとコレクション) が用意されており、その動作原理は次のとおりです。

  1. アソシエーション タグまたはコレクション タグが fetchType="lazy" で構成されている場合、MyBatis はクエリ結果でこの属性のプロキシ オブジェクトを返します。このプロキシ オブジェクトには、ターゲット プロパティのデータ読み込みロジックが含まれています。
  2. 初めてプロキシ オブジェクトの getter メソッドを呼び出すと、プロキシ オブジェクトはクエリを実行して実際のデータをロードし、それ自体を実際のデータ オブジェクトに置き換えます。
  3. 次に、getter メソッドを呼び出すと、実際のデータ オブジェクトが直接返されます。

このようにして、属性の遅延ロードが実現され、属性が本当に必要な場合にのみクエリが実行されてデータがロードされます。

例を見てみましょう:

ユーザーとアカウントの構成:

<resultMap id="userAccountMap" type="User">
  <association property="account" select="selectAccountByUserId" column="id" fetchType="lazy"/> 
</resultMap>

<select id="selectUserById" resultMap="userAccountMap">
  select * from user where id = #{id}
</select>

<select id="selectAccountByUserId" resultType="Account">
  select * from account where user_id = #{id}
</select>

テストコード:

User user = mapper.selectUserById(1);
// 第一次调用account属性的getter方法
Account accout = user.getAccount();  
  • 実行するとselectUserById(1)、User オブジェクトがクエリされ、その account プロパティがプロキシ オブジェクトとして入力されます。
  • 初めて呼び出されたときuser.getAccount()、プロキシ オブジェクトはselectAccountByUserId実際の Account データをクエリし、それ自体を実際の Account オブジェクトに置き換えます。
  • 後で呼び出すと、getAccount()Account オブジェクトが直接返されます。

そこで問題は、このプロキシ オブジェクトとは何でしょうか? どの選択ステートメントを実行するかをどのようにして知るのでしょうか?

MyBatis は JDK 動的プロキシを使用し、InvocationHandler を拡張して遅延読み込みを実現します。fetchType=lazy の場合、MyBatis は次のプロキシ オブジェクトを作成します。

Proxy.newProxyInstance(
  mapperInterface.getClassLoader(), 
  new Class[] {
    
     mapperInterface },
  new LazyLoaderHandler(sqlSession, selectStatement)
)
  • LazyLoaderHandler は、実行される実際の select ステートメントを認識する InvocationHandler インターフェイスを実装します。
  • getter メソッドが呼び出されるとき、実際には InvocationHandler の invoke メソッドが呼び出されます。
  • invoke メソッドは、まず属性が読み込まれているかどうかを判断し、読み込まれていない場合は、select ステートメントを実行して SqlSession を通じてデータをクエリし、結果をソース オブジェクトに設定します。
  • 次に、invoke メソッドを呼び出すと、ロードされたデータが直接返されます。

ここに画像の説明を挿入

要約すると、MyBatis の遅延読み込みの原理は次のとおりです。

  1. 遅延読み込みロジックを含むプロキシ オブジェクトは、JDK 動的プロキシを通じて生成されます。
  2. getter メソッドが初めて呼び出されるとき、プロキシ オブジェクトを通じて select ステートメントが実行され、データがクエリされます。
  3. クエリ結果をソース オブジェクトに設定し、結果を返します。
  4. getter メソッドを呼び出した後、遅延読み込みを実現するために結果が直接返されます。

MyBatis の遅延ロード機能の基盤となる JDK ダイナミック プロキシは、InvocationHandler を強化することで遅延ロード プロキシ効果を実現します。これは、MyBatis のこの機能の独創的な実装です。

遅延読み込みを使用すると、実際に使用する必要があるまで、関連する属性の読み込みを遅らせることができます。これにより、データベースの対話効率が大幅に向上し、メモリ消費が削減されます。これは、MyBatis がデータベースのパフォーマンスを最適化するための重要な手段の 1 つです。

内部構造

ここに画像の説明を挿入

プロキシの後に、Bean には MethodHandler が含まれます。このメソッドハンドラーには、実行される遅延ロードを保存するための Map が含まれており、遅延ロードが実行される前に削除されます。LoadPair は、逆シリアル化された Bean の実行環境を準備するために使用されます。ResultLoader はロード操作を実行するために使用されます。実行前に元のエグゼキュータが閉じられた場合は、新しいエグゼキュータが作成されます。

特定の属性のロードに失敗した場合、再度ロードされることはありません。

Beanプロキシプロセス

プロキシ プロセスは、結果セットが解析されて作成された後に発生します (DefaultResultSetHandler.createResultObject)。対応するプロパティが遅延読み込みに設定されている場合、元のオブジェクトを継承する ProxyFactory を通じてプロキシ オブジェクトが作成され、オブジェクトのすべての値がプロキシ オブジェクトにコピーされます。そして、対応する MethodHandler を設定します (元のオブジェクトは直接破棄されます)。

ここに画像の説明を挿入

ユニオンクエリとネストされたマップ

マッピングの説明

マッピングとは、返された ResultSet 列と Java Bean プロパティ間の対応関係を指します。マッピングの記述はResultMappingを通じて行われ、ResultMapで全体としてパッケージ化されます。マッピングは、単純なマッピングと複合ネストされたマッピングに分けられます。

简单映射: つまり、返された結果セット列には、オブジェクト属性と 1 対 1 の関係があります。この場合、ResultHandler は結果セット内の行を順番に走査し、各行にオブジェクトを作成して、走査結果セット列にオブジェクトのマップされた属性を入力します。

ここに画像の説明を挿入
嵌套映射: しかし、ほとんどの場合、オブジェクト構造はツリーレベルのプロセスです。つまり、オブジェクトにはオブジェクトが含まれます。対応するマッピングもこの種の入れ子構造になります。

ここに画像の説明を挿入
設定モードでは、サブマッピングを直接設定したり、外部マッピングや自動マッピングを導入したりできます。入れ子構造には 2 つのタイプがあります。

  • 1対多
  • 多対多

ここに画像の説明を挿入
公式 Web サイトには、マッピングの使用方法に関する非常に詳細なドキュメントがあります。ここでは詳細には触れません。次に、ネストされたマッピング結果セットの充填プロセスを分析します。

共同クエリ

マッピングを行った後に結果を取得するにはどうすればよいですか? 通常の単一テーブル クエリでは、複合マッピングで必要な結果を取得できないため、結合クエリを使用する必要があります。次に、結合クエリによって返されたデータ列をさまざまなオブジェクト属性に分割します。1 対 1 と 1 対多も同じ方法で分割して作成されます。

1対1のクエリマッピング

select a.id,
       a.title,
       b.id as user_id,
       b.name as user_name
from blog a
         left join users b on a.author_id=b.id
where a.id = 1;

上記のステートメントとクエリ ステートメントを組み合わせると、次の表の結果が得られます。結果の最初の 2 つのフィールドはブログに対応し、最後の 2 つのフィールドはユーザーに対応します。次に、ブログ オブジェクトの作成者属性としてユーザーを入力します。

ここに画像の説明を挿入
ここに画像の説明を挿入
上の 2 つの例では、各行で Blog 親オブジェクトと User 子オブジェクトという 2 つのオブジェクトが生成されます。

1対多のクエリ

select a.id,a.title,
       c.id as comment_id,
       c.body as comment_body
from blog a
         left join comment c on a.id=c.blog_id
where a.id = 1;

上記のステートメントは 3 つの結果を取得できます。最初の 2 つのフィールドは Blog に対応し、後の 2 つのフィールドは Comment に対応します。1 対 1 とは異なり、3 本の線は同じブログを指します。IDが同じだから。

ここに画像の説明を挿入
ここに画像の説明を挿入
上記の結果では、同じ 3 行のブログでブログが作成され、同時に 3 つの異なるコメントを作成してコレクションを形成し、それをコメント オブジェクトに埋め込みます。

RowKey 作成メカニズム

1 対多のクエリ プロセスでは、RowKey に基づいて 2 つのデータ行が同じかどうかが判断されます。RowKey は通常、 に基づいています。ただし、RowKey の作成に他のマッピング フィールドが使用されることが指定されていない場合があります。具体的なルールは次のとおりです。

ここに画像の説明を挿入

結果セットの解析プロセス

ここでは、1 対 1 は 1 対多の簡略化されたバージョンであるため、1 対多の状況が分析に直接使用されます。クエリの結果は次のとおりです。

ここに画像の説明を挿入
分析プロセス全体は次のとおりです。

ここに画像の説明を挿入

フローの説明:

すべてのマッピング プロセスの解析は、DefaultResultSetHandler で行われます。主な方法は次のとおりです。

  • handleRowValuesForNestedResultMap(): ネストされた結果セット解析エントリ。結果セット内のすべての行が走査されます。そして行ごとに RowKey オブジェクトを作成します。次に、getRowValue() を呼び出して、解析結果オブジェクトを取得します。最後にResultHandlerに保存しました。

    • 注: getRowValue を呼び出す前に、解析されたオブジェクトが RowKey に基づいて取得され、partialObject パラメータとして getRowValue に送信されます。
  • getRowValue(): このメソッドは最終的に、現在の行に基づいて解析されたオブジェクトを生成します。具体的な責任には、1. オブジェクトの作成、2. 共通プロパティの設定、3. ネストされたプロパティの設定が含まれます。ネストされたプロパティを解析するとき、getRowValue が再帰的に呼び出され、サブオブジェクトを取得します。最後のステップ 4. RowKey に基づいて現在の解析オブジェクトを一時的に保存します。

    • PartialObject パラメータが空でない場合は、ステップ 3 のみが実行されます。1と2はすでに実行されているためです。
  • applyNestedResultMappings(): ネストされた結果セット マッピングを解析して設定し、すべてのネストされたマッピングを走査して、そのネストされた ResultMap を取得します。次に、RowKey を作成して一時記憶領域の値を取得します。次に、getRowValue を呼び出して属性オブジェクトを取得します。最後に親オブジェクトに埋め込みます。

    • RowKey を通じて属性オブジェクトを取得できる場合でも、その下に未解決の属性が存在する可能性があるため、getRowsValue が呼び出されます。

おすすめ

転載: blog.csdn.net/zyb18507175502/article/details/130869546