2016-02-16 7 views
1

У меня есть справочная база данных, которая содержит координаты объектов на небесной сетке. Я хочу запросить базу данных и найти объекты, которые «близки к» (в пределах определенного углового расстояния) данной точки.Как я могу запросить набор данных, основанный на двойных столбцах, «близких к» некоторым значениям?

Я попробовал этот запрос:

const double WithinOneMinute = 1.0/60.0; // 1 minute of arc 
var db = CompositionRoot.GetTargetDatabase(); 
var targets = from item in db.Targets 
       where item.RightAscension.IsCloseTo(ra.Value, WithinOneMinute) 
        && item.Declination.IsCloseTo(dec.Value, WithinOneMinute) 
       select item; 
var found = targets.ToList(); 

Это терпит неудачу, потому что поставщик запроса LINQ не понимает мой IsCloseTo метод расширения, который реализуется как:

public static bool IsCloseTo(this double comparand, double comparison, double tolerance = EightDecimalPlaces) 
{ 
    var difference = Math.Abs(comparand - comparison); 
    return (difference <= tolerance); 
} 

Так что я в настоящее время застрял в идеях. Кто-нибудь сделал что-нибудь подобное?

ответ

1

Проблема заключается в том, что Entity Framework не знает, как перевести это на SQL. Вместо фильтрации после того, как вы попадаете в базу данных, вы можете просто включить в свой запрос фильтр, как если бы вы писали прямой SQL. Он будет немного более подробным, но будет намного дешевле перетащить большое количество данных, которые будут немедленно отфильтрованы после того, как они будут построены в памяти.

Что вам нужно сделать, это сравнить каждое ваше время с высоким и низким, что вы ищете.

// I prefer to move these outside the query for clarity. 
var raPlus = ra.Value.AddMinute(1); 
var raMinus = ra.Value.AddMinute(-1); 
var decPlus = dec.Value.AddMinute(1); 
var decMinus = dec.Value.AddMinute(-1); 

var targets = from item in db.Targets 
       where item.RightAscension <= raPlus && 
        item.RightAscension >= raMinus && 
        item.Declination <= decPlus && 
        item.Declination >= decMinus 
       select item; 
+0

Принимая это как ответ - но я отправляю свой собственный ответ, чтобы точно показать, как я в конце концов это сделал. –

+0

Ах, извините за то, что вы недостаточно думали о проблеме, чтобы понять, что вы имели в виду угловые «часы» и «минуты», а не время. Но принцип по-прежнему сохраняется, как вы продемонстрировали в своем ответе. – krillgar

0

Поставщик запросов LINQ (to EF) не знает, как выполнить ваш метод IsCloseTo в SQL. Вы должны перечислить ваши детали первого и чем фильтровать его с помощью метода расширения вам, что-то вроде этого:

var db = CompositionRoot.GetTargetDatabase(); 
var targets = from item in db.Targets 
       select item; 
//now targets will be enumarated and can be querable with LINQ to objects 
var filteredTargets = from target in targets.ToList() 
       where target.RightAscension.IsCloseTo(ra.Value, WithinOneMinute) 
       && target.Declination.IsCloseTo(dec.Value, WithinOneMinute) 
       select target; 
var filteredTargets = targets.ToList(); 
+0

Это может быть ** очень ** дорого, если есть много записей, которые будут отфильтрованы. – krillgar

+0

Единственный вариант - не использовать метод расширения. – danteus

+0

В настоящее время существует около 100 записей, но это сильно увеличится, поэтому включение всех строк на самом деле не является вариантом. Однако я решил проблему другим способом, я отправлю свой собственный ответ. –

2

Как вы уже заметили, пользовательские функции не могут использоваться как часть дерева выражений запроса. Таким образом, вам нужно либо встроить логику функций вручную внутри запроса, тем самым внедрить много дублирования кода или переключиться на синтаксис метода и использовать вспомогательные методы, которые возвращают целое выражение.

Позже это можно сделать вручную, используя методы System.Linq.Expressions, но это не является естественным и требует больших знаний. Позвольте мне представить вам более простой способ.

Цель будет реализовать метод расширения, как этот

public static IQueryable<T> WhereIsCloseTo<T>(this IQueryable<T> source, Expression<Func<T, double>> comparand, double comparison, double tolerance = EightDecimalPlaces) 
{ 
    return source.Where(...); 
} 

и использовать его следующим образом

var targets = db.Targets 
    .WhereIsCloseTo(item => item.RightAscension, ra.Value, WithinOneMinute) 
    .WhereIsCloseTo(item => item.Declination, dec.Value, WithinOneMinute); 

Обратите внимание, что при таком подходе вы не можете использовать &&, но прикован Where производить эквивалентный результат ,

Во-первых, позвольте представить выражение эквивалент исходной функции

public static Expression<Func<double, bool>> IsCloseTo(double comparison, double tolerance = EightDecimalPlaces) 
{ 
    return comparand => Math.Abs(comparand - comparison) >= tolerance; 
} 

Проблема заключается в том, что он не может быть использован непосредственно в нашем методе, так как он нуждается в Expression<Func<T, bool>>.

К счастью, это можно легко сделать, используя маленький помощник утилита сформировать свой ответ на Define part of an Expression as a variable in c#:

public static class ExpressionUtils 
{ 
    public static Expression<Func<TOuter, TResult>> Bind<TOuter, TInner, TResult>(this Expression<Func<TOuter, TInner>> source, Expression<Func<TInner, TResult>> resultSelector) 
    { 
     var body = new ParameterExpressionReplacer { source = resultSelector.Parameters[0], target = source.Body }.Visit(resultSelector.Body); 
     var lambda = Expression.Lambda<Func<TOuter, TResult>>(body, source.Parameters); 
     return lambda; 
    } 

    public static Expression<Func<TOuter, TResult>> ApplyTo<TInner, TResult, TOuter>(this Expression<Func<TInner, TResult>> source, Expression<Func<TOuter, TInner>> innerSelector) 
    { 
     return innerSelector.Bind(source); 
    } 

    class ParameterExpressionReplacer : ExpressionVisitor 
    { 
     public ParameterExpression source; 
     public Expression target; 
     protected override Expression VisitParameter(ParameterExpression node) 
     { 
      return node == source ? target : base.VisitParameter(node); 
     } 
    } 
} 

Теперь у нас есть все необходимое, так что наш метод реализован простой, как это:

public static IQueryable<T> WhereIsCloseTo<T>(this IQueryable<T> source, Expression<Func<T, double>> comparand, double comparison, double tolerance = EightDecimalPlaces) 
{ 
    return source.Where(IsCloseTo(comparison, tolerance).ApplyTo(comparand)); 
} 
+0

Спасибо, Иван. Это, вероятно, было излишним для моих непосредственных потребностей, но это действительно интересно, и я отпишу его для дальнейшего использования. Я уверен, что я найду для этого какое-то использование, поэтому спасибо, что нашли время для публикации. –

0

Это, как я в конце концов сделал это:

 const double radius = 1.0;   
     const double radiusHours = radius/15.0; 
     var db = CompositionRoot.GetTargetDatabase(); 
     var ra = rightAscension.Value;  
     var dec = declination.Value;   
     var minRa = ra - radiusHours; 
     var maxRa = ra + radiusHours; 
     var minDec = dec - radius; 
     var maxDec = dec + radius; 
     var closeTargets = from target in db.Targets 
          where target.RightAscension >= minRa 
           && target.RightAscension <= maxRa 
           && target.Declination >= minDec 
           && target.Declination <= maxDec 
          let deltaRa = Abs(target.RightAscension - ra) * 15.0 // in degrees 
          let deltaDec = Abs(target.Declination - dec) 
          let distanceSquared = deltaRa * deltaRa + deltaDec * deltaDec 
          orderby distanceSquared 
          select target; 

Один SLI ght осложнение заключается в том, что Right Ascension находится в часах (15 градусов в час), тогда как склонение находится в градусах, поэтому я должен приспособиться к этому в нескольких местах.

Я сначала сужу список к объектам в пределах небольшого радиуса (в данном случае 1 градус). На самом деле я использую квадрат 1 градус, но это достаточно хорошее приближение. Затем я заказываю предметы на расстоянии от требуемой точки, используя Pythagoras (я не беру квадратный корень, потому что это дает ошибку снова, но опять же это достаточно хорошо, чтобы просто получить правильный порядок).

Затем, наконец, я материализую запрос и беру первый элемент в качестве своего ответа.

Это все еще не идеально, потому что оно не обрабатывает случай, когда Right Ascension близок к нулю. В итоге я сравню с отрицательным RA вместо чего-то около 23:59, но пока я могу жить с ним.

Результаты приводят речевой синтезатор, который «объявляет», где телескоп указывает как имя объекта. Довольно круто :) Если я иногда скучаю по одному, это не имеет большого значения.

 Смежные вопросы

  • Нет связанных вопросов^_^