黒龍's Blog

明日から役立つ無駄知識をあなたに(仮)

ホーム 連絡をする 同期する ( RSS 2.0 ) Login
投稿数  170  : 記事  0  : コメント  2719  : トラックバック  26

ニュース

わんくま同盟に参加させていただきました。
どうぞよろしくお願いします。

自己紹介

コミュニティ

  • わんくま同盟
    わんくま同盟

書庫

2012年1月10日 #

前回(Linq to 色々その3.汎用的な呼び出しのベースを作成する。)、ひとまずクエリを投げて取得する汎用的なものは作成できました。今回はページング対応のものを完成させることにします。ページングで汎用的なものを作ればグルメサーチも店名サーチも両方使えますね!

今回ページングするにあたってSkipとTakeは必要そうなのでInnermostWhereFinderをInnermostFinderとリネームして中身にSkip、Takeの取得を追加します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace LinqToHotpepper
{
    internal class InnermostFinder : ExpressionVisitor
    {
        private MethodCallExpression innermostWhereExpression;
        private MethodCallExpression innermostSkipExpression;
        private MethodCallExpression innermostTakeExpression;

        public MethodCallExpression GetInnermostWhere(Expression expression)
        {
            Visit(expression);
            return innermostWhereExpression;
        }

        public MethodCallExpression GetInnermostSkip(Expression expression)
        {
            Visit(expression);
            return innermostSkipExpression;
        }
        public MethodCallExpression GetInnermostTake(Expression expression)
        {
            Visit(expression);
            return innermostTakeExpression;
        }
        protected override Expression VisitMethodCall(MethodCallExpression expression)
        {
            if (expression.Method.Name == "Where")
                innermostWhereExpression = expression;

            if (expression.Method.Name == "Skip")
                innermostSkipExpression = expression;
            
            if (expression.Method.Name == "Take")
                innermostTakeExpression = expression;

            Visit(expression.Arguments[0]);
            return expression;
        }
    }
}
Modifierも併せて修正。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace LinqToHotpepper
{
    public static class ExpressionTreeModifier
    {
        public static ExpressionTreeModifier<T> Create<T>(IQueryable<T> query)
        {
            return new ExpressionTreeModifier<T>(query);
        }
    }
    public class ExpressionTreeModifier<T> : ExpressionVisitor
    {
        private IQueryable<T> queryable;

        internal ExpressionTreeModifier(IQueryable<T> query)
        {
            this.queryable = query;
        }

        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (node.Method.Name == "Where")
                return Visit(node.Arguments[0]);

            if (node.Method.Name == "Skip")
                return Visit(node.Arguments[0]);
            
            if (node.Method.Name == "Take")
                return Visit(node.Arguments[0]);
            
            return base.VisitMethodCall(node);
        }

        protected override Expression VisitConstant(ConstantExpression c)
        {
            // Replace the constant Query<T> arg with the queryable Place collection.
            if (c.Type == typeof(Query<T>))
                return Expression.Constant(this.queryable);
            else
                return c;
        }
    }
}

Whereとおんなじ感じで追加。で、Whereの抽出部分を以前はQueryProviderBase<T>の中でやってたんですがSkipはページングしない場合は必要ないのでWhereの解釈部分も派生クラス側でやるように修正。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace LinqToHotpepper
{
    public abstract class QueryProviderBase<T> : IQueryProvider
    {
        public IQueryable CreateQuery(Expression expression)
        {
            Type elementType = TypeSystem.GetElementType(expression.Type);
            try
            {
                return (IQueryable)Activator.CreateInstance(typeof(Query<>).MakeGenericType(elementType), new object[] { this, expression });
            }
            catch (System.Reflection.TargetInvocationException tie)
            {
                throw tie.InnerException;
            }
        }

        // Queryable's collection-returning standard query operators call this method.
        public IQueryable<TResult> CreateQuery<TResult>(Expression expression)
        {
            return new Query<TResult>(this, expression);
        }

        public object Execute(Expression expression)
        {
            return this.Execute<object>(expression, false);
        }

        // Queryable's "single value" standard query operators call this method.
        // It is also called from QueryableGourmetData.GetEnumerator().
        public TResult Execute<TResult>(Expression expression)
        {
            bool IsEnumerable = (typeof(TResult).Name == "IEnumerable`1");

            return this.Execute<TResult>(expression, IsEnumerable);
        }

        public TResult Execute<TResult>(Expression expression, bool IsEnumerable)
        {
            // The expression must represent a query over the data source.
            if (!IsQueryOverDataSource(expression))
                throw new InvalidProgramException("No query over the data source was specified.");

            IQueryable<T> result = GetResult(expression).AsQueryable<T>();

            var treeCopier = ExpressionTreeModifier.Create(result);
            Expression newExpressionTree = treeCopier.Visit(expression);

            // This step creates an IQueryable that executes by replacing Queryable methods with Enumerable methods.
            if (IsEnumerable)
                return (TResult)result.Provider.CreateQuery(newExpressionTree);
            else
                return (TResult)result.Provider.Execute(newExpressionTree);
        }

        private bool IsQueryOverDataSource(Expression expression)
        {
            // If expression represents an unqueried IQueryable data source instance,
            // expression is of type ConstantExpression, not MethodCallExpression.
            return (expression is MethodCallExpression);
        }

        protected abstract IEnumerable<T> GetResult(Expression expression);
    }
}

あとはページング対応のProviderを実装。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Xml.Linq;
using System.Xml;
using System.Net;

namespace LinqToHotpepper
{
    /// <summary>
    /// ファクトリクラス
    /// </summary>
    public static class PagingQueryProvider
    {
        /// <summary>
        /// ファクトリメソッド
        /// </summary>
        /// <param name="apiKey">発行されたAPIキー</param>
        /// <param name="baseUrl">リクエストURL</param>
        /// <param name="name">抽出対象のXML要素名</param>
        /// <param name="builder">結果のファクトリ</param>
        public static PagingQueryProvider<T> Create<T>(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder)
        {
            return new PagingQueryProvider<T>(apiKey, baseUrl, name, builder, null);
        }
        /// <summary>
        /// ファクトリメソッド
        /// </summary>
        /// <param name="apiKey">発行されたAPIキー</param>
        /// <param name="baseUrl">リクエストURL</param>
        /// <param name="name">抽出対象のXML要素名</param>
        /// <param name="builder">結果のファクトリ</param>
        /// <param name="finder">要素から抽出を行うFinder</param>
        public static PagingQueryProvider<T> Create<T>(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder, EqualsFinder<T> finder)
        {
            return new PagingQueryProvider<T>(apiKey, baseUrl, name, builder, finder);
        }
    }
    public class PagingQueryProvider<T> : QueryProviderBase<T>
    {
        /// <summary>発行されたAPIキー</summary>
        private string key;
        /// <summary>リクエストURL</summary>
        private string baseUrl;
        /// <summary>要素から抽出を行うFinder</summary>
        private EqualsFinder<T> finder;
        /// <summary>抽出対象のXML要素名</summary>
        private string name;
        /// <summary>結果セットのファクトリ</summary>
        private Func<IEnumerable<XElement>, T> resultBuilder;
        /// <summary>開始件数</summary>
        private int start;
        /// <summary>取得中件数</summary>
        private int index;
        /// <summary>パラメータで指定された取得件数</summary>
        private int? takeCount;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="apiKey">発行されたAPIキー</param>
        /// <param name="baseUrl">リクエストURL</param>
        /// <param name="name">抽出対象のXML要素名</param>
        /// <param name="builder">結果のファクトリ</param>
        /// <param name="finder">要素から抽出を行うFinder</param>
        public PagingQueryProvider(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder, EqualsFinder<T> finder)
        {
            this.start = 0;
            this.key = apiKey;
            this.baseUrl = baseUrl;
            this.name = name;
            this.resultBuilder = builder;
            this.finder = finder;
        }

        protected override IEnumerable<T> GetResult(Expression expression)
        {
            InnermostFinder innerFinder = new InnermostFinder();
            var sb = new StringBuilder(baseUrl);
            sb.AppendFormat("?key={0}", key);
            if (finder != null)
            {
                MethodCallExpression whereExpression = innerFinder.GetInnermostWhere(expression);
                if (whereExpression != null)
                {
                    LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(whereExpression.Arguments[1])).Operand;
                    // Send the lambda expression through the partial evaluator.
                    lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);
                    finder.Expression = lambdaExpression.Body;

                    foreach (var item in finder.Results)
                    {
                        sb.AppendFormat("&{0}={1}", item.Key, item.Value);
                    }
                }
            }

            MethodCallExpression skipExpression = innerFinder.GetInnermostSkip(expression);
            if (skipExpression != null)
            {
                this.start = int.Parse(ExpressionTreeHelpers.GetValueFromExpression(skipExpression.Arguments[1]));
            }

            MethodCallExpression takeExpression = innerFinder.GetInnermostTake(expression);
            if (takeExpression != null)
            {
                this.takeCount = int.Parse(ExpressionTreeHelpers.GetValueFromExpression(takeExpression.Arguments[1]));
            }

            var url = sb.ToString();

            var executeUrl = url + string.Format("&start={0}&count=30", start + 1);
            var element = XDocument.Load(executeUrl);

            //エラーチェック
            var error = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}error").FirstOrDefault();
            if (error != null) throw new InvalidQueryException(new Error(error).Message);

            //クエリー条件にマッチする、検索結果の全件数
            var total = int.Parse(element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_available").FirstOrDefault().Value);
            //このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
            var returned = int.Parse(element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_returned").FirstOrDefault().Value);
            //検索結果の開始位置
            var resultStart = int.Parse(element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_start").FirstOrDefault().Value);

            var collection = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}" + name);

            var limit =total;

            if (takeCount.HasValue) limit = takeCount.Value + start;

            //残りがあるなら残りのリクエストをシーケンスにくっつけておく
            if (resultStart + returned < limit)
            {
                //次の呼び出しのためにカウンタを進めておく
                index = start + returned;
                collection = collection.Concat(this.GetResultCore(url));
            }
            if (takeCount.HasValue)
            {
                collection.Take(takeCount.Value);
            }
            yield return resultBuilder(collection);
        }
        /// <summary>
        /// 繰り返し部分の取得実体
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        private IEnumerable<XElement> GetResultCore(string url)
        {
            //取得ループ開始
            while (true)
            {
                var executeUrl = url + string.Format("&start={0}&count=30", index + 1);

                var element = XDocument.Load(executeUrl);

                //エラーチェック
                var error = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}error").FirstOrDefault();
                if (error != null) throw new InvalidQueryException(new Error(error).Message);

                //クエリー条件にマッチする、検索結果の全件数
                var total = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_available").FirstOrDefault();
                // このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
                var returned = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_returned").FirstOrDefault();
                //結果がなければ終了
                if (total == null || total.Value == "0" || returned == null || returned.Value == "0") yield break;

                var collection = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}" + name);

                var limit = int.Parse(total.Value);

                if (takeCount.HasValue) limit = takeCount.Value + start;

                foreach (var item in collection)
                {
                    index++;
                    if(index > limit) yield break;
                    yield return item;
                }
            }
        }
    }
}
ひとまずこれで完成。

他のWebServiceもこれらをベースにすればLinq化するのも楽かもしれません。まぁWebはXML or JSONで十分って気がしないでもないですが^^;
posted @ 1:37 | Feedback (12)