いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

iBatis.NET奮闘記-006 (1対Nのデータ取得) (Mapper, ISqlMapper, QueryForObject, groupBy resultMap)


今回は、1対Nのデータの取得を行なってみます。


やり方ですが、基本的にはJava版と同じです。


1対NのようなJOIN関連の場合に重要となってくるのが
SQL定義ファイル内に定義するresultMapの定義です。

これが、無いとJOINがうまく出来ません。


実際に、どのように書くのかというと、以下のようにします。

    <resultMaps>
        <resultMap id="FindByCategoryIdResultMap" class="CategoryAndMemos" groupBy="CategoryId">
            <result property="CategoryId"   column="CategoryId"/>
            <result property="CategoryName" column="CategoryName"/>
            <result property="Memos"        resultMapping="CategoryAndMemos.FindByCategoryIdResultMap-Memo"/>
        </resultMap>

        <resultMap id="FindByCategoryIdResultMap-Memo" class="Memo">
            <result property="MemoId"                column="MemoId"/>
            <result property="MemoTitle"             column="MemoTitle"/>
            <result property="MemoData"              column="MemoData"/>
            <result property="MemoAuthor.AuthorId"   column="AuthorId"/>
            <result property="MemoAuthor.AuthorName" column="AuthorName"/>
        </resultMap>
    </resultMaps>

上記の定義は、今回のSQL定義ファイルの中から抜き出したものです。
見たら大体お分かりだと、思いますが

<result property="オブジェクトのプロパティ名"   column="対応するSQLの結果列の名前"/>

という風に、定義していきます。


てことで、今回のサンプルです。
今回は、以下のデータが予め登録されているとします。

  • Categoriesテーブル
    • CategoryIdが1で、CategoryNameが"C#"となっているデータが存在する。
  • Authorsテーブル
    • AuthorIdが10で、Nameが"gsf_zero1"と成っているデータが存在する。
    • AuthorIdが11で、Nameが"gsf_zero2"と成っているデータが存在する。
  • Memosテーブル

以下のデータが存在するとする。

MemoId CategoryId AuthorId Title MemoData
1 1 10 C#-001 MemoData-001
2 1 11 C#-002 MemoData-002


上記の状態で、カテゴリから紐付くメモデータを取得します。
また、メモデータには、対応する作者データを付加します。


まず、データモデルクラスから。


作者データを表すクラスです。
[Author.cs]

using System;
using System.Collections.Generic;
using System.Text;

namespace Gsf.Samples.IBatisNet.Models {
    
    [Serializable]
    public class Author {

        int    _authorId = -1;
        string _authorName;

        public int AuthorId{
            get{
                return _authorId;
            }
            protected set{
                _authorId = value;
            }
        }

        public string AuthorName{
            get{
                return _authorName;
            }
            set{
                _authorName = value;
            }
        }
    }
}


次は、メモデータを表すクラスです。
[Memo.cs]

using System;

namespace Gsf.Samples.IBatisNet.Models {
    
    [Serializable]
    public class Memo {

        int    _memoId;
        string _title;
        string _memoData;
        /// <summary>
        /// メモデータの作者を表します。
        /// </summary>
        Author _author;

        public int MemoId{
            get{
                return _memoId;
            }
            protected set{
                _memoId = value;
            }
        }

        public string MemoTitle{
            get{
                return _title;
            }
            set{
                _title = value;
            }
        }

        public string MemoData{
            get{
                return _memoData;
            }
            set{
                _memoData = value;
            }
        }

        public Author MemoAuthor{
            get{
                return _author;
            }
            set{
                _author = value;
            }
        }
    }
}


最後に、カテゴリデータとそれに紐付くメモデータを表すクラスです。

[CategoryAndMemos.cs]

using System;
using System.Collections.Generic;


namespace Gsf.Samples.IBatisNet.Models {

    [Serializable]
    public class CategoryAndMemos {

        int        _categoryId = -1;
        string     _categoryName;
        /// <summary>
        /// カテゴリに紐付くメモデータのリスト
        /// </summary>
        List<Memo> _memos;

        public int CategoryId{
            get{
                return _categoryId;
            }
            protected set{
                _categoryId = value;
            }
        }

        public string CategoryName{
            get{
                return _categoryName;
            }
            set{
                _categoryName = value;
            }
        }

        public List<Memo> Memos{
            get{
                return _memos;
            }
            protected set{
                _memos = value;
            }
        }
    }
}


次は、上記のクラスにデータをマッピングするためのSQL定義ファイルです。

<?xml version="1.0" encoding="utf-8" ?>
<sqlMap namespace="CategoryAndMemos"
        xmlns="http://ibatis.apache.org/mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <alias>
        <typeAlias type="Gsf.Samples.IBatisNet.Models.Memo"             alias="Memo"/>
        <typeAlias type="Gsf.Samples.IBatisNet.Models.CategoryAndMemos" alias="CategoryAndMemos"/>
    </alias>

    <resultMaps>
        <resultMap id="FindByCategoryIdResultMap" class="CategoryAndMemos" groupBy="CategoryId">
            <result property="CategoryId"   column="CategoryId"/>
            <result property="CategoryName" column="CategoryName"/>
            <result property="Memos"        resultMapping="CategoryAndMemos.FindByCategoryIdResultMap-Memo"/>
        </resultMap>

        <resultMap id="FindByCategoryIdResultMap-Memo" class="Memo">
            <result property="MemoId"                column="MemoId"/>
            <result property="MemoTitle"             column="MemoTitle"/>
            <result property="MemoData"              column="MemoData"/>
            <result property="MemoAuthor.AuthorId"   column="AuthorId"/>
            <result property="MemoAuthor.AuthorName" column="AuthorName"/>
        </resultMap>
    </resultMaps>
    
    <statements>
        <select id="FindByCategoryId" parameterClass="int" resultMap="FindByCategoryIdResultMap">
            <![CDATA[
                select
                     c.CategoryId   as CategoryId
                    ,c.CategoryName as CategoryName
                    ,m.MemoId       as MemoId
                    ,m.Title        as MemoTitle
                    ,m.MemoData     as MemoData
                    ,a.AuthorId     as AuthorId
                    ,a.Name         as AuthorName
                from
                    Categories c,
                    Memos      m,
                    Authors    a
                where
                    c.CategoryId = #value#
                    and
                    c.CategoryId = m.CategoryId
                    and
                    m.AuthorId   = a.AuthorId
                order by
                    m.MemoId
            ]]>
        </select>
    </statements>
</sqlMap>


重要な点は、以下の部分です。
>|
<resultMap id="FindByCategoryIdResultMap" class="CategoryAndMemos" groupBy="CategoryId">

<

groupByという属性がついていますが、これを指定することにより、SQLを実行した結果を指定したプロパティ値に
基づいてグループ化してくれます。
つまり、SQLをそのまま実行すると以下のようになっているデータを

1 'C#' 1 'C#-001' 'MemoData-001' 10 'gsf_zero1'
1 'C#' 2 'C#-002' 'MemoData-002' 11 'gsf_zero2'


以下のようにしてくれます。

1 'C#' 1 'C#-001' 'MemoData-001' 10 'gsf_zero1'
    2 'C#-002' 'MemoData-002' 11 'gsf_zero2'


次に重要なのが、以下の部分です。

            <result property="Memos"        resultMapping="CategoryAndMemos.FindByCategoryIdResultMap-Memo"/>

この部分は、Memosというプロパティに対して、resultMappingで指定した結果マッピングを行なった結果をセットしなさいと
いう風になります。ibatisは、該当するプロパティの方がList<Memo>であり、結果が複数あることも認識しますので
これで、該当のリストオブジェクトにグループ化された分のデータがセットされます。


尚、データをマッピングする際に、先程のCategoryAndMemosクラスの方では、Memosプロパティのリストオブジェクトをnewして
いませんでしたが、ibatis側がリフレクションを使用して型を判別し、インスタンス化もしれくれますので、事前に
オブジェクトを作成しておく必要はありません。また、Memoクラスの方のAuthorプロパティも同じく、ibatisインスタンス化を
面倒みてくれますので、問題ありません。


最後に、実行サンプルです。

using System;
using System.Collections.Generic;

using NUnit.Framework;

using Gsf.Samples.IBatisNet.Models;

using IBatisNet.DataMapper;
using IBatisNet.Common;

namespace Gsf.Samples.IBatisNet {

    [TestFixture]
    public class IBatisNetSample006 {

        [Test]
        public void JOINの動作を確認してみる(){
            //
            // データ取得.
            //
            CategoryAndMemos categoryAndMemos = 
                Mapper.Instance().QueryForObject<CategoryAndMemos>("CategoryAndMemos.FindByCategoryId", 1);

            //
            // データがちゃんと取得できているかどうかを確認
            //
            Assert.IsNotNull(categoryAndMemos);
            Assert.AreEqual(1,    categoryAndMemos.CategoryId);
            Assert.AreEqual("C#", categoryAndMemos.CategoryName);
            Assert.AreEqual(2,    categoryAndMemos.Memos.Count);
            categoryAndMemos.Memos.ForEach(delegate(Memo target){ 
                Assert.IsNotNull(target);
                Assert.IsNotNull(target.MemoAuthor);
            });

            PrintCategoryAndMemos(categoryAndMemos);
        }

        void PrintCategoryAndMemos(CategoryAndMemos categoryAndMemos){
            Console.WriteLine("CategoryId:{0}",   categoryAndMemos.CategoryId);
            Console.WriteLine("CategoryName:{0}", categoryAndMemos.CategoryName);

            foreach(Memo m in categoryAndMemos.Memos){
                Console.WriteLine("\tMemoId:{0}",    m.MemoId);
                Console.WriteLine("\tMemoTitle:{0}", m.MemoTitle);
                Console.WriteLine("\tMemoData:{0}",  m.MemoData);

                Author author = m.MemoAuthor;
                Console.WriteLine("\t\tAuthorId:{0}",   author.AuthorId);
                Console.WriteLine("\t\tAuthorName:{0}", author.AuthorName);
            }
        }
    }

    class DummyEntryPoint006{
        static void Main(){
            //
            // noop;
            //
        }
    }
}


上記のサンプルを実行すると、以下のように表示されます。

 ------ Test started: Assembly: Sample006.exe ------

CategoryId:1
CategoryName:C#
	MemoId:1
	MemoTitle:C#-001
	MemoData:MemoData-001
		AuthorId:10
		AuthorName:gsf_zero1
	MemoId:2
	MemoTitle:C#-002
	MemoData:MemoData-002
		AuthorId:11
		AuthorName:gsf_zero2


一回のSQL呼び出しで、紐付いたデータが取得でき、マッピングも正常に行なわれている事が確認できます。