Сложные отношения между таблицами в NHibernate

Я пишу отображение Fluent NHibernate для устаревшей базы данных Oracle. Проблема в том, что таблицы имеют составные первичные ключи. Если бы у меня была полная свобода, я бы перепроектировал отношения и автоматически сгенерировал первичные ключи, но другие приложения должны писать в ту же базу данных и читать из нее, поэтому я не могу этого сделать.

Вот две таблицы, на которых я сосредоточусь:

альтернативный текст

Пример данных

Trips table:
1, 10:00, 11:00 ...
1, 12:00, 15:00 ...
1, 16:00, 19:00 ...
2, 12:00, 13:00 ...
3, 9:00, 18:00 ...

Faults table:
1, 13:00 ...
1, 23:00 ...
2, 12:30 ...

В данном случае ТС 1 совершило три поездки и имеет две неисправности. Первая неисправность произошла во время второй поездки, а вторая – во время стоянки автомобиля. Автомобиль 2 совершил одну поездку, во время которой возникла неисправность.

Ограничения

Поездки одного и того же транспортного средства никогда не пересекаются. Таким образом, таблицы имеют необязательную связь "один ко многим", потому что каждый сбой либо происходит во время поездки, либо нет. Если бы я хотел присоединиться к ним в SQL, я бы написал:

select ... 
from Faults left outer join Trips
  on Faults.VehicleId = Trips.VehicleId
  and Faults.FaultTime between Trips.TripStartTime and Trips.TripEndTime

а затем я получу набор данных, в котором каждая ошибка появляется ровно один раз (как я уже сказал, один ко многим).

Обратите внимание, что таблицы Vehicles нет, да она мне и не нужна. Но я создал представление, содержащее все VehicleId из обеих таблиц, поэтому я могу использовать его как соединительную таблицу.

Что я на самом деле ищу?

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

Поэтому мне нужно сопоставление и критерии, которые будут запускать что-то вроде следующего ниже SQL:

select ... 
from Faults left outer join Trips
  on Faults.VehicleId = Trips.VehicleId
  and Faults.FaultTime between Trips.TripStartTime and Trips.TripEndTime
where Faults.FaultTime between :p0 and :p1

У вас есть идеи, как этого добиться?

Примечание 1. В настоящее время приложение не должно выполнять запись в базу данных, поэтому постоянство не является обязательным, хотя, если сопоставление поддерживает постоянство, это может помочь в будущем.

Примечание 2: я знаю, что это сложный вопрос, поэтому, если вы дадите мне отличный ответ, вы будете должным образом вознаграждены :)

Спасибо, что прочитали этот длинный вопрос, и теперь я надеюсь только на лучшее :)


person Ilya Kogan    schedule 05.01.2011    source источник
comment
Какую версию NH вы используете? NH3 с поддержкой Linq2NH?   -  person cdmdotnet    schedule 11.01.2011


Ответы (5)


Текущая рекомендация

Учитывая дополнительную информацию в комментариях, теперь я бы предложил попробовать следующие сопоставления классов вместо использования каких-либо пользовательских решений SQL, упомянутых ниже в этом ответе:

<class name="Fault" table="Faults">
  <composite-id>
    <key-property name="VehicleId" />
    <key-property name="FaultTime" />
    <key-property name="FaultType" />
    <generator class="assigned" />
  </id> 
  <many-to-one name="Trip" class="Trip">
    <!-- Composite Key of Trip is calculated on the fly -->
    <formula>VehicleId</formula>
    <formula>
      ( SELECT  TripStartTime 
        FROM    Trips t 
        WHERE   VehicleId = t.VehicleId 
        AND     FaultTime BETWEEN t.TripStartTime AND t.TripEndTime
      )
    </formula>
  </many-to-one>
  ...
</class> 

<class name="Trip" table="Trips">
  <composite-id>
    <key-property name="VehicleId" />
    <key-property name="TripStartTime" />
  </composite-id> 
  ...
</class>

Используя это сопоставление, вы можете загружать и запрашивать объекты Fault, как вам нравится.

Устаревшие предложения

Первоначально я рассматривал здесь (названный) пользовательский SQL-запрос. Вы можете ввести следующий запрос в свой файл сопоставления, чтобы загрузить объекты Fault для данного автомобиля:

<sql-query name="LoadFaultsAndTrips" xml:space="preserve">
  <return class="Fault" alias="f"/>
  <return-join alias="t" property="f.Trip"/>
  SELECT  {f.*}
      ,   {t.*}
  FROM    Faults f
  LEFT OUTER JOIN Trips t 
      ON f.VehicleId = t.VehicleId
      AND f.FaultTime BETWEEN t.TripStartTime AND t.TripEndTime
  WHERE f.VehicleId = ?
</sql-query>

Если вам нужно загрузить коллекцию Faults для объекта Vehicle без явных запросов, вы можете попробовать следующую конструкцию сопоставления в XML:

<class name="Vehicle">
   <id name="VehicleId" type="...">
     <generator class="..." />
   </id>
   ...
   <bag name="Faults" table="Faults" inverse="true">
     <key column="VehicleId" />
     <loader query-ref="VehicleFaultsLoader" />
   </bag>
   ...
</class>

<sql-query name="VehicleFaultsLoader" xml:space="preserve">
  <load-collection role="Vehicle.Faults" alias="f" />
  <return-join alias="t" property="f.Trip"/>
  SELECT  {f.*}
      ,   {t.*}
  FROM    Faults f
  LEFT OUTER JOIN Trips t 
      ON f.VehicleId = t.VehicleId
      AND f.FaultTime BETWEEN t.TripStartTime AND t.TripEndTime
  WHERE f.VehicleId = ?
</sql-query>

Ключевым моментом здесь является определение пользовательского загрузчика коллекции для коллекции Faults в классе Vehicle и определение пользовательского SQL-запроса, который получает первичный ключ Vehicle в качестве параметра. Я еще не использовал свободный NHibernate, поэтому боюсь, что не могу помочь вам с этой частью вопроса.

Здоровья, Герке.

person Gerke Geurts    schedule 09.01.2011
comment
Спасибо, буду искать способы перевести на Fluent. - person Ilya Kogan; 09.01.2011
comment
Ваш ответ был очень поучительным. И все же это не решает всех проблем, поэтому у меня есть два дополнительных вопроса: 1. Я никогда не хочу выбирать ВСЕ поездки и неисправности автомобиля. Я только хочу выбрать некоторые в зависимости от времени. Я думаю, что мне нужна ссылка «многие к одному» между Fault и Trip на основе этого сложного запроса. Как выбрать неисправности и отключения по времени? 2. Будет ли в первом запросе NH создавать объекты Fault и Trip из полей, полученных в результате запроса? Результаты выглядят как набор данных с большим количеством столбцов, так что достаточно ли умен NH, чтобы разделить их на объекты? - person Ilya Kogan; 09.01.2011
comment
И еще вопрос: не означает ли предложенное вами сопоставление, что у меня есть таблица Vehicles? (потому что я не... Я могу создать представление «Транспортные средства», но это все еще не решает проблему загрузки по времени). - person Ilya Kogan; 09.01.2011
comment
Похоже, что первый именованный запрос - это путь в вашем сценарии. Вы можете добавить дополнительные параметры, чтобы ограничить возвращаемые строки. Этот запрос вернет экземпляры Fault с опционально установленным свойством Trip (на самом деле с использованием ManyToOneMapping для свойства Trip в классе Fault). - person Gerke Geurts; 10.01.2011
comment
Просто чтобы уточнить, вместо WHERE VehicleId = ? в запросе LoadFaultsAndTrips вы можете ввести любой другой оператор WHERE со всеми необходимыми параметрами. - person Gerke Geurts; 10.01.2011
comment
Привет @Gerke. То, что я ищу, - это построение отношений между сущностями, которые позволят мне написать fault.Trip или, по крайней мере, fault.Trips. Это то, что делает первый именованный запрос? Должен ли я тогда не упоминать взаимосвязь между Fault и Trip в отображении? - person Ilya Kogan; 11.01.2011

У вас пример sql там синтаксически такой же как

select ... 
from Faults left join Trips
  on Faults.VehicleId = Trips.VehicleId
where Faults.VehicleId is null or (Faults.FaultTime between Trips.TripStartTime and Trips.TripEndTime)

имея это в виду, вы можете создать обычную карту, например (свободно)

HasMany< Trip >( fault => fault.Trips )
    .KeyColumn( "VehicleId" )
    .Table( "Trips" )
    .LazyLoad( )
    .Cascade.Delete( )
    .AsSet()

затем, используя любую удобную форму запроса, будь то hql, icriteria, icriteriaover или linq, выполните стандартный запрос с предложением where, как указано выше.

в linq это будет:

IList<Trip> results = 
( 
    fault in Session.Query< Entities.Faults > 
    join trip in Session.Query< Entities.Trips > on fault.VehicleId equals trip.VehicleId into trip
    where
    fault.FaultTime > startTime && fault.FaultTime < endTime &&
    // Here is the rest of the join criteria expressed as a where criteria
    (
        trip == null
            || 
        (
            fault.FaultTime > trip.TripStartTime && fault.FaultTime < trip.TripEndTime
        ) 
    )
    select fault
).ToList();

Если нужно, могу привести пример в ICriteria или IQueryOver.

Конечно, это работает только потому, что приведенный вами пример оператора может быть переписан как предложение where, имея результат. Если в реальном мире желаемый sql более сложен, вам нужно подумать, можно ли переписать желаемый sql при архивировании того же результата.

person cdmdotnet    schedule 11.01.2011

Я новичок в NH и знаю только зачатки NH, поэтому, когда я сталкивался с такой ситуацией, я писал хранимую процедуру, а затем вызвал его через NH. В конце концов я найду решение, полностью основанное на NH, а затем проведу рефакторинг кода и устраню необходимость в хранимой процедуре.

Другой подход, который может сработать, — просто написать HQL нужно.

person arcain    schedule 09.01.2011
comment
Спасибо за ссылку на HQL. После вашего ответа я прочитал некоторую информацию о HQL, но до сих пор не понял, как его применить в моем случае. Не могли бы вы дать мне более конкретное представление о том, что делать? Тем временем я также пробую подход @Gerke. В любом случае, хранимые процедуры - это не то, к чему я хочу найти свой путь, если только я не должен. - person Ilya Kogan; 09.01.2011
comment
@ Илья Коган - Да, мои методы - это крайние меры, и я думал о том же, что и ответ cdmdotnet, но используя HQL, а не linq. - person arcain; 12.01.2011

Я сделаю предложение, если вы используете NHibernate 3, попробуйте Linq to NH. Используя Linq, вы можете указать ручные/произвольные отношения для однократного выполнения или использовать каналы, если вы думаете, что они будут повторно использоваться (или быть linq, если вы хотите выполнить левое/правое соединение, вам нужно указать это, если это при соединении isser вам вообще не нужно указывать соединение, все это выводится из сопоставлений) и является бизнес-логикой, а не логикой постоянства.

В качестве быстрого примера это будет что-то вроде:

var result = ( 
fault in Session.Query< Entities.Faults > 
join trip in Session.Query< Entities.Trips > on fault.VehicleId equals trip.VehicleId into trip
where 
fault.FaultTime > startTime && fault.FaultTime < endTime &&
fault.FaultTime > trip.TripStartTime && fault.FaultTime < trip.TripEndTime
select fault
).ToList();

Я написал это от руки, так что это может быть не идеально, но достаточно близко. Это должно делать именно то, что вам нужно, и позволит вам изменить его по своему усмотрению, не меняя сопоставления.

person cdmdotnet    schedule 10.01.2011
comment
На самом деле я пытался использовать LINQ to NH, но это не дает никакой дополнительной ценности, кроме более ясного синтаксиса. < и > не переводятся в < и > в SQL. LINQ to HN выполняет оператор SELECT, который загружает всю таблицу, а затем возвращает только часть результатов. В любом случае, даже если бы ограничения сработали, это всего лишь метод запросов, и он все равно не позволит мне писать такие вещи, как fault.Trip в моем коде, что мне и нужно. - person Ilya Kogan; 11.01.2011
comment
Чтобы решить вашу проблему с fault.Trip, если вы изменили select fault на что-то более подходящее. затем вы можете получить либо раздражающий тип с любой структурой, которую вы хотите, либо спроецировать его в конкретный класс, такой как ваш класс Trip. Что касается вашего комментария об отсутствии добавленной стоимости, как я уже сказал, произвольные отношения во время выполнения имеют свое применение. Но я предполагаю, что это может не подходить для этого приложения. - person cdmdotnet; 11.01.2011

Если вы уже знаете, какой запрос вы хотите, чтобы БД выполняла, почему бы просто не выполнить запрос напрямую, используя свой собственный класс DAO? Зачем возиться с абстракцией NHibernate, если она только мешает?

person benjismith    schedule 05.01.2011
comment
Потому что у NHibernate много других преимуществ. Это правда, что этот конкретный запрос является крепким орешком, но остальная часть приложения получит большую пользу от NHibernate, и я уверен, что решу и эту проблему. - person Ilya Kogan; 06.01.2011
comment
Кроме того, тот факт, что я знаю, какой запрос я хочу выполнить, не имеет значения. Любой, кто знает SQL, может написать любой запрос к любой базе данных не хуже, чем OR-M, но это не значит, что OR-M бесполезны. - person Ilya Kogan; 06.01.2011