Фильтрация данных по дате и времени в SharePoint и Java

Фильтрация данных по дате и времени — одна из самых распространенных, но при этом коварных задач в разработке. Различия в часовых поясах, форматах хранения (UTC vs Local), точности (миллисекунды, наносекунды) и синтаксисе запросов часто приводят к багам, которые трудно воспроизвести и отладить. В этой статье мы подробно сравним подходы к фильтрации по дате в экосистеме Microsoft SharePoint (CAML, REST, OData, KQL) и на платформе Java (классические java.util.Date, современный java.time, JPA/Specification, Stream API).

Содержание

  1. Фундаментальные различия: UTC, часовые пояса и точность
  2. Фильтрация по дате в SharePoint
    1. CAML-запросы (Server-Side Object Model / CSOM)
    2. REST API и OData $filter
    3. KQL (Keyword Query Language) для Поиска
    4. DASL и WebDAV (исторический контекст)
  3. Фильтрация по дате в Java
    1. Legacy API: java.util.Date и Calendar (антипаттерны)
    2. Modern API: java.time (LocalDate, ZonedDateTime, Instant)
    3. Работа с БД: JPA Criteria API, Specifications, QueryDSL
    4. In-memory фильтрация: Stream API и паттерны проектирования
  4. Сравнительная таблица и ключевые отличия
  5. Best Practices и частые ошибки

Фундаментальные различия: UTC, часовые пояса и точность

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

  • SharePoint хранит все значения полей типа DateTime в формате UTC в базе данных контента. При отображении в UI происходит конвертация в часовой пояс пользователя (Regional Settings) или сайта. Любой запрос к API (CAML, REST, Search) должен оперировать UTC.
  • Java (java.time) разделяет понятия:
    • Instant — момент на временной шкале (UTC, наносекунды). Аналог DateTime в SP.
    • ZonedDateTime — момент + часовой пояс (правила перехода на летнее/зимнее время).
    • LocalDateTime / LocalDate — дата/время БЕЗ привязки к зоне (опасно для фильтрации моментов времени).
  • Точность: SharePoint (SQL Server backend) обычно округляет до 1/300 секунды (3.33 мс) или использует datetime2 (100 нс). Java Instant — наносекунды. При сравнении «равенства» всегда используйте диапазоны (Range), а не точное совпадение (Equals).

Важное напоминание из документации Microsoft (DASL/DAV): «Запросы DASL всегда выполняют сравнение даты и времени в формате UTC, поэтому если вы применяете литерал даты в строке сравнения, то для сравнения вам необходимо использовать его значение в формате UTC.» — Это правило актуально для всех современных API SharePoint.


Фильтрация по дате в SharePoint

CAML-запросы (SSOM / CSOM / PnPjs)

CAML (Collaborative Application Markup Language) — это основной язык запросов для серверной и клиентской объектной модели. Фильтрация по дате требует строгого соблюдения формата ISO 8601 в UTC.

Синтаксис элемента <Value>

  • Type=»DateTime» — для полей даты и времени.
  • Type=»DateOnly» — если нужно сравнивать только дату (игнорируя время), доступно в современных версиях SharePoint (2016/2019/Online);
  • IncludeTimeValue=»TRUE/FALSE», атрибут для DateTime, позволяющий игнорировать время при сравнении (устаревший подход, лучше использовать DateOnly).

Пример: Элементы, измененные после определенной даты (UTC)

<Where>
 <Gt>
 <FieldRef Name='Modified' />
 <Value Type='DateTime'>2026-06-08T15:00:00Z</Value>
 </Gt>

</Where>

Пример: Фильтр «Сегодня» (динамическое значение)

В CAML есть специальные токены для относительных дат:

<Where>
 <Eq>
 <FieldRef Name='Created' />
 <Value Type='DateTime'><Today /></Value>
 </Eq>
</Where>
  • <Today />, текущая дата в часовом поясе сайта (или пользователя? Зависит от контекста выполнения, обычно сайта).
  • <Today OffsetDays='-7' /> — 7 дней назад.
  • <Month />, <Year /> — для группировки.

Операторы сравнения

  • <Eq>, <Neq> (Равно / Не равно)
  • <Gt>, <Geq> (Строго больше / Больше или равно)
  • <Lt>, <Leq> (Строго меньше / Меньше или равно)
  • <In> / <NotIn> (для наборов дат, редко используется)

REST API и OData $filter

Современный стандарт для SharePoint Online и On-Prem (2013+). Использует синтаксис OData v4 (в основном).

Формат даты

Обязательно формат ISO 8601: YYYY-MM-DDTHH:mm:ssZ (UTC) или с.offset YYYY-MM-DDTHH:mm:ss+03:00. Рекомендуется всегда использовать Z (Zulu/UTC).

Примеры запросов

  • Больше даты (после): _api/web/lists/getbytitle('Tasks')/items?$filter=Modified gt 2026-06-08T15:00:00Z
  • Диапазон (между): $filter=Created ge 2026-06-01T00:00:00Z and Created le 2026-06-30T23:59:59Z
  • Только дата (игнорируя время): В OData v4 есть функции year, month, day.
    $filter=year(Created) eq 2026 and month(Created) eq 6 and day(Created) eq 8

    Внимание: Эти функции выполняются на стороне сервера, но могут не использовать индексы эффективно. Лучше использовать диапазон ge startOfDay and lt startOfNextDay.

Динамические значения (Current Date)

В чистом OData/REST нет встроенной функции now или today в $filter (в отличие от CAML). Дату нужно вычислять на клиенте (JavaScript/TypeScript/C#) и подставлять как литерал.

KQL (Keyword Query Language) — Поиск (Search API)

Используется для запросов к Search Index (_api/search/query). Синтаксис отличается от CAML/OData.

Синтаксис

  • Операторы: : (равно), >, >=, <, <=, .. (диапазон).
  • Формат даты: Строго ISO 8601 UTC (например, 2026-06-08T15:00:00Z). Локальное время в KQL не поддерживается напрямую в строке запроса.
  • Управляемые свойства (Managed Properties) должны быть типа DateTime и иметь флаг Queryable/Refinable;

Примеры

  • Modified>=2026-06-08T00:00:00Z
  • Created:2026-06-08 — KQL умеет парсить «дату без времени» как начало дня в UTC (но лучше явно указывать диапазон).
  • Modified:2026-06-01;.2026-06-30 — диапазон включительно.

DASL и WebDAV (Исторический контекст / Outlook/Exchange)

В предоставленной справке упоминается DASL (DAV Searching and Locating) в контексте Outlook/Exchange (DAV:getlastmodified). В современном SharePoint это практически не используется, но принцип остается тем же: строка сравнения должна быть в UTC.

criteria = "@SQL=" & Chr(34) & "DAV:getlastmodified" & Chr(34) & " < " & Format(UTC_Date, "General Date")

Фильтрация по дате в Java

Java прошла долгий путь от java.util.Date (JDK 1.0) через Calendar к современному java.time (JSR-310, JDK 8+). Использование Legacy API в новом коде считается антипаттерном;

Legacy API: java.util.Date и Calendar (Антипаттерны)

  • java.util.Date, это просто обертка над long (миллисекунды с эпохи UTC). У него нет часового пояса.
  • SimpleDateFormat — не потокобезопасен. Использование в статических полях ведет к порче данных при параллелизме.
  • Calendar — громоздкий, мутабельный, сложный API.

Проблема сравнения «без времени» (Legacy)

Частая задача: «найти записи за сегодня». В Legacy это делалось через обнуление времени:

// ОПАСНО: Зависит от дефолтного TimeZone JVM!
Calendar cal = Calendar.getInstance; 
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
Date startOfDay = cal.getTime;
// ... запрос в БД: where date_col >= ? and date_col < ? (startOfNextDay)

Главная ошибка: Забыть про TimeZone. Если сервер в UTC, а пользователь в MSK+3, "начало дня" будет разным.

Modern API: java.time (Стандарт де-факто)

Пакет java.time — неизменяемый (immutable), потокобезопасный, разделяет понятия.

Ключевые классы для фильтрации

Класс Описание Когда использовать для фильтрации
Instant Момент времени (UTC, наносекунды) Хранение в БД (TIMESTAMP WITH TIME ZONE), передача между системами, логирование.
ZonedDateTime Момент + ZoneId (правила зоны) Ввод/вывод пользователя, бизнес-логика "в часовом поясе клиента", планировщики.
LocalDate Только дата (без зоны и времени) Фильтрация "за день" (День рождения, Отчет за ), если точное время не важно.
OffsetDateTime Момент + фиксированный offset (+03:00) Работа с API, которые не знают ZoneId (только смещение), JSON сериализация.

Правильное сравнение дат (Best Practice)

Никогда не используйте equals для моментов времени (из-за наносекунд). Используйте диапазоны (Half-Open Intervals [start, end)).

// Пример: Найти все события, произошедшие 8 июня 2026 года в Москве
ZoneId moscow = ZoneId.of("Europe/Moscow");
LocalDate targetDate = LocalDate.of(2026, 6, 8);

// 1. Определяем границы дня в целевой зоне
ZonedDateTime startOfDay = targetDate.atStartOfDay(moscow); // 2026-06-08T00:00+03:00
ZonedDateTime endOfDay = targetDate.plusDays(1).atStartOfDay(moscow); // 2026-06-09T00:00+03:00
// 2. Конвертируем в Instant (UTC) для запроса к БД/Индексу
Instant startInstant = startOfDay.toInstant; // 2026-06-07T21:00:00Z
Instant endInstant = endOfDay.toInstant; // 2026-06-08T21:00:00Z

// 3. Фильтр: timestamp >= startInstant AND timestamp < endInstant
// Это использует индексы БД и корректно работает с DST (лето/зима)

Работа с БД: JPA Criteria API, Specifications, QueryDSL

В Enterprise Java фильтрация обычно строится динамически.

Паттерн Specification (JPA Specification Executor)

Как упоминалось в источнике (StackOverflow), для избежания "комбинаторного взрыва" условных операторов (if (param != null) ...) используется паттерн Specification (или Criteria Builder).

public class EventSpecifications {

 public static Specification<Event> createdBetween(Instant start, Instant end) {
 return (root, query, cb) -> {
 if (start == null && end == null) return null; // Нет фильтра
 
 List<Predicate> predicates = new ArrayList<>;
 if (start != null) {
 predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), start));
 }
 if (end != null) {
 predicates.add(cb.lessThan(root.get("createdAt"), end)); // Строго меньше конца
 }
 return cb;and(predicates.toArray(new Predicate[0]));
 };
 }
 
 // Комбинирование: specs.and(createdBetween(s, e)).and(authorIs(user))
}

QueryDSL (Type-safe SQL)

Позволяет писать QEvent.event.createdAt.goe(start).and(QEvent.event.createdAt.lt(end)). Безопаснее рефакторингом.

Маппинг типов в Hibernate/JPA

  • Instant -> TIMESTAMP WITH TIME ZONE (PostgreSQL), DATETIMEOFFSET (SQL Server). Лучший выбор.
  • LocalDateTime -> TIMESTAMP WITHOUT TIME ZONE; Опасно, если серверы в разных зонах.
  • LocalDate -> DATE.

Совет: Всегда храните Instant (UTC) в БД. Конвертацию в зону пользователя делайте на уровне представления (DTO/Controller).

In-memory фильтрация: Stream API и паттерны проектирования

Если данные уже загружены в память (List, Set), используется Stream API.

List<Event> events = eventRepository.findAll; // Загрузили все (осторожно с памятью!)
Instant now = Instant.now;
List<Event> futureEvents = events.stream
 .filter(e -> e.getCreatedAt.isAfter(now)) // Instant имеет isAfter/isBefore
 .collect(Collectors.toList);

Архитектура сложных фильтров: Chain of Responsibility / Decorator

Как указано в источнике (StackOverflow: "Архитектура построения сложной системы фильтрации"), для гибкой комбинации фильтров в памяти или как построителей запросов используется Chain of Responsibility (Цепочка обязанностей);

// Интерфейс фильтра
@FunctionalInterface
public interface EventFilter {
 Stream<Event> apply(Stream<Event> stream, FilterContext ctx);
}
// Реализации-цепочки
public class DateRangeFilter implements EventFilter {
 public Stream<Event> apply(Stream<Event> stream, FilterContext ctx) {
 if (ctx.getStart != null) stream = stream.filter(e -> !e.getCreatedAt.isBefore(ctx.getStart));
 if (ctx.getEnd != null) stream = stream.filter(e -> e.getCreatedAt.isBefore(ctx.getEnd));
 return stream;
 }
}

public class AuthorFilter implements EventFilter { ... }

// Использование (Pipeline)
List<EventFilter> filters = List.of(new DateRangeFilter, new AuthorFilter, new TypeFilter);
Stream<Event> result = events.stream;
for (EventFilter f : filters) {
 result = f.apply(result, context);
}

Это позволяет добавлять новые критерии фильтрации без изменения существующего кода (Open/Closed Principle).


Сравнительная таблица: SharePoint vs Java

  • CAML: Type='DateOnly' или IncludeTimeValue='FALSE'.
  • REST: day, month, year или диапазон ge startOfDayZ lt startOfNextDayZ.
  • KQL: Диапазон date..date.
  • Характеристика SharePoint (CAML / REST / KQL) Java (java.time / JPA / Streams)
    Хранение даты Всегда UTC в БД контента (SQL). Рекомендуется Instant (UTC) в БД. LocalDateTime — только если зона фиксирована/неважна.
    Формат запроса ISO 8601 String (2026-06-08T15:00:00Z). Объекты Instant/LocalDate (Parameter Binding в JDBC/JPA). Никаких строк в коде!
    Часовые пояса Настройка на уровне Site Collection / User Profile. API принимает UTC. <Today/> в CAML использует зону сайта. Явное управление через ZoneId. ZonedDateTime для ввода, Instant для хранения/фильтра.
    Фильтр "За день" (без времени) Стандарт: LocalDate.atStartOfDay(zone).toInstant -> диапазон [startInstant, endInstant). Работает с DST корректно.
    Динамические даты ("Сегодня", "Неделя назад") CAML: <Today OffsetDays='-7'/> (на стороне сервера).
    REST/OData: Нет встроенного, считать на клиенте.
    Полный контроль на клиенте/сервере: LocalDate.now(zone).minusDays(7). Тестируемо (можно замокать Clock).
    Точность сравнения Зависит от версии SQL (обычно 3.33мс или 100нс). Equals опасен. Instant (наносекунды). Equals почти всегда false. Только Range (ge/lt).
    Архитектура сложных фильтров Сборка XML (CAML) или строки $filter (REST) через StringBuilder / библиотеки (PnPjs, sp-query). Specification Pattern, QueryDSL, CriteriaBuilder, Stream Pipeline (Chain of Responsibility). Типобезопасно.
    Индексация и производительность Зависит от колонки (индексирована ли?). KQL использует Search Index (асинхронно). Зависит от БД и типа колонки. Instant + индекс = быстро. Функции над колонкой (year(col)) убивают индекс (Index Seek -> Scan).

    Best Practices и частые ошибки (Checklist)

    Общие (И SharePoint, и Java)

    1. Никогда не сравнивайте даты на точное равенство (Equals / Eq). Всегда используйте полуоткрытые интервалы [StartInclusive, EndExclusive).
    2. Всегда указывайте часовой пояс явно. Не полагайтесь на "дефолтный" (JVM Default Zone, SharePoint Regional Settings). Передавайте ZoneId / TimeZone как параметр контекста пользователя.
    3. Храните UTC (Instant). Конвертируйте в Local/Zoned только на границе системы (UI, API входящий/исходящий).
    4. Избегайте функций над колонками в WHERE (year(Created) = 2026, CAST(Created AS DATE), TRUNC(Created)). Это предотвращает использование индексов (Index Scan). Используйте расчет границ диапазона в коде приложения.

    Специфично для SharePoint

    • Используйте PnPjs (TypeScript/JS) или PnP.Framework (C#) для построения CAML/OData, они сами корректно форматируют даты в UTC ISO 8601.
    • Для "Сегодня" в CAML используйте <Today /> (считывает зону сайта). В REST — вычисляйте на клиенте new Date.toISOString.
    • Поле Modified / Created — всегда UTC. Пользовательские поля DateTime — тоже UTC в БД, но в формах могут отображаться в локали пользователя.
    • При миграции (CSOM/REST) — читайте FieldDateTimeValue или парсьте строку ISO, не пытайтесь парсить локализованные строки (" 18:00").

    Специфично для Java

    • Забудьте про java.util.Date, Calendar, SimpleDateFormat (кроме интеграции с Legacy библиотеками). Используйте java.time.*.
    • Используйте DateTimeFormatter.ISO_INSTANT или ISO_OFFSET_DATE_TIME для JSON (Jackson настраивается автоматически).
    • Для тестируемости времени внедряйте java.time.Clock (Dependency Injection), а не вызывайте Instant.now / LocalDate.now напрямую в бизнес-логике.
    • В JPA: маппите Instant на TIMESTAMP WITH TIME ZONE (Postgres) / DATETIMEOFFSET (MSSQL). Для LocalDate — DATE.
    • Сложные динамические фильтры, только через Specification / QueryDSL / CriteriaBuilder. Избегайте конкатенации строк SQL/JPQL.

    Несмотря на кажущуюся простоту, фильтрация по дате — это область, где архитектурные решения определяют корректность системы на годах вперед.

    • SharePoint диктует жесткие правила: UTC в ISO 8601 на входе API. Мощь CAML в токенах (<Today/>) для серверной логики, а REST/OData/KQL требуют клиентского расчета границ. Главная ловушка — неявное использование часового пояса сайта в CAML <Today/> против явного UTC в REST.
    • Java дает мощный, типобезопасный инструментарий (java.time). Ключ к успеху — разделение Instant (для хранения и фильтрации) и ZonedDateTime/LocalDate (для бизнес-правил и UI). Паттерны Specification и Chain of Responsibility позволяют строить сложные фильтры без "спагетти-кода".

    Главный универсальный принцип: Вычисляйте границы диапазона (Start Inclusive, End Exclusive) в коде приложения с явным указанием часового пояса, передавайте их в запрос как параметры (Instant/UTC), и ваши фильтры будут быстрыми, корректными относительно летнего/зимнего времени и переносимыми между платформами.


    Статья подготовлена на основе актуальных данных на . Версии: SharePoint Online / 2019, Java 21 LTS (java.time), Jakarta Persistence 3.1.