Фильтрация данных по дате и времени — одна из самых распространенных, но при этом коварных задач в разработке. Различия в часовых поясах, форматах хранения (UTC vs Local), точности (миллисекунды, наносекунды) и синтаксисе запросов часто приводят к багам, которые трудно воспроизвести и отладить. В этой статье мы подробно сравним подходы к фильтрации по дате в экосистеме Microsoft SharePoint (CAML, REST, OData, KQL) и на платформе Java (классические java.util.Date, современный java.time, JPA/Specification, Stream API).
Содержание
- Фундаментальные различия: UTC, часовые пояса и точность
- Фильтрация по дате в SharePoint
- CAML-запросы (Server-Side Object Model / CSOM)
- REST API и OData $filter
- KQL (Keyword Query Language) для Поиска
- DASL и WebDAV (исторический контекст)
- Фильтрация по дате в Java
- Legacy API: java.util.Date и Calendar (антипаттерны)
- Modern API: java.time (LocalDate, ZonedDateTime, Instant)
- Работа с БД: JPA Criteria API, Specifications, QueryDSL
- In-memory фильтрация: Stream API и паттерны проектирования
- Сравнительная таблица и ключевые отличия
- 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 нс). JavaInstant— наносекунды. При сравнении «равенства» всегда используйте диапазоны (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:00ZCreated: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
| Характеристика | 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)
- Никогда не сравнивайте даты на точное равенство (Equals / Eq). Всегда используйте полуоткрытые интервалы
[StartInclusive, EndExclusive). - Всегда указывайте часовой пояс явно. Не полагайтесь на "дефолтный" (JVM Default Zone, SharePoint Regional Settings). Передавайте
ZoneId/TimeZoneкак параметр контекста пользователя. - Храните UTC (Instant). Конвертируйте в Local/Zoned только на границе системы (UI, API входящий/исходящий).
- Избегайте функций над колонками в 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.