Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Как я взаимодействовал с MongoDB
В последних проектах использовал MongoDB как основное хранилище документов. Расскажу про реальный опыт и best practices.
Проект: Event Tracking System
Мы создавали систему отслеживания событий пользователей (analytics). MongoDB идеально подходила для неструктурированных данных о событиях.
Архитектура
Application Layer
↓
Mongo Repository (Spring Data MongoDB)
↓
MongoDB (NoSQL Database)
1. Настройка проекта
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<version>3.0.0</version>
</dependency>
application.yml
spring:
data:
mongodb:
uri: mongodb://localhost:27017/analytics_db
# или раздельно:
# host: localhost
# port: 27017
# database: analytics_db
# username: admin
# password: secret
2. Document Model (Entity)
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import com.fasterxml.jackson.annotation.JsonProperty;
@Document(collection = "user_events")
public class UserEvent {
@Id // Объект будет использовать _id из MongoDB
private String id;
@Field("userId")
private String userId;
@Field("eventType")
private String eventType; // "page_view", "click", "purchase" и т.д.
@Field("eventData")
private Map<String, Object> eventData; // Неструктурированные данные
@Field("timestamp")
private LocalDateTime timestamp;
@Field("sessionId")
private String sessionId;
@Field("ipAddress")
private String ipAddress;
// Nested документ
@Field("device")
private DeviceInfo device;
// Constructors, getters, setters
public UserEvent() {}
public UserEvent(String userId, String eventType,
Map<String, Object> eventData) {
this.userId = userId;
this.eventType = eventType;
this.eventData = eventData;
this.timestamp = LocalDateTime.now(ZoneId.of("UTC"));
}
}
@Document // Nested документ
public class DeviceInfo {
private String os; // "iOS", "Android", "Windows"
private String browser; // "Chrome", "Safari"
private String screenResolution;
// Getters, setters...
}
3. MongoDB Repository
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import java.time.LocalDateTime;
import java.util.List;
public interface UserEventRepository extends MongoRepository<UserEvent, String> {
// Простой поиск
List<UserEvent> findByUserId(String userId);
// Поиск с типом события
List<UserEvent> findByUserIdAndEventType(String userId, String eventType);
// Поиск в диапазоне времени
List<UserEvent> findByTimestampBetween(LocalDateTime start, LocalDateTime end);
// Кастомный query
@Query("{ 'userId': ?0, 'eventType': ?1 }")
List<UserEvent> findUserEventsByType(String userId, String eventType);
// Более сложный query
@Query("{ 'userId': ?0, 'timestamp': { $gte: ?1, $lte: ?2 } }")
List<UserEvent> findEventsByUserAndDateRange(String userId,
LocalDateTime start,
LocalDateTime end);
// С проекцией (выбираем только нужные поля)
@Query(value = "{ 'userId': ?0 }",
fields = "{ 'userId': 1, 'eventType': 1, 'timestamp': 1 }")
List<UserEvent> findUserEventsWithoutData(String userId);
// Удаление
long deleteByUserIdAndTimestampBefore(String userId, LocalDateTime cutoff);
}
4. Service Layer
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.stereotype.Service;
@Service
public class EventService {
private final UserEventRepository eventRepository;
private final MongoTemplate mongoTemplate;
public EventService(UserEventRepository eventRepository,
MongoTemplate mongoTemplate) {
this.eventRepository = eventRepository;
this.mongoTemplate = mongoTemplate;
}
// Сохранение события
public UserEvent recordEvent(String userId, String eventType,
Map<String, Object> data) {
UserEvent event = new UserEvent(userId, eventType, data);
return eventRepository.save(event);
}
// Получение всех событий пользователя за период
public List<UserEvent> getUserEventsByPeriod(String userId,
LocalDateTime start,
LocalDateTime end) {
return eventRepository.findEventsByUserAndDateRange(userId, start, end);
}
// Агрегация: подсчёт событий по типам
public Map<String, Long> getEventStatistics(String userId) {
List<AggregationOperation> pipeline = Arrays.asList(
Aggregation.match(Criteria.where("userId").is(userId)),
Aggregation.group("eventType").count().as("count"),
Aggregation.project("_id", "count")
);
AggregationResults<Map> results = mongoTemplate.aggregate(
Aggregation.newAggregation(pipeline),
"user_events",
Map.class
);
Map<String, Long> stats = new HashMap<>();
for (Map entry : results.getMappedResults()) {
stats.put((String) entry.get("_id"),
((Number) entry.get("count")).longValue());
}
return stats;
}
// Более сложная агрегация
public List<Map> getUserActivityTimeline(String userId) {
List<AggregationOperation> pipeline = Arrays.asList(
Aggregation.match(Criteria.where("userId").is(userId)),
Aggregation.sort(Sort.Direction.DESC, "timestamp"),
Aggregation.limit(100),
Aggregation.group("eventType")
.count().as("total")
.push(new BasicDBObject("date", "$timestamp")
.append("count", 1)).as("timeline")
);
AggregationResults<Map> results = mongoTemplate.aggregate(
Aggregation.newAggregation(pipeline),
"user_events",
Map.class
);
return results.getMappedResults();
}
// Bulk операция
public void deleteOldEvents(int daysToKeep) {
LocalDateTime cutoff = LocalDateTime.now(ZoneId.of("UTC"))
.minusDays(daysToKeep);
eventRepository.deleteByUserIdAndTimestampBefore(null, cutoff);
}
}
5. REST Controller
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
@RestController
@RequestMapping("/api/v1/events")
public class EventController {
private final EventService eventService;
public EventController(EventService eventService) {
this.eventService = eventService;
}
@PostMapping
public ResponseEntity<UserEvent> recordEvent(
@RequestBody EventRequest request) {
UserEvent event = eventService.recordEvent(
request.getUserId(),
request.getEventType(),
request.getData()
);
return ResponseEntity.created(null).body(event);
}
@GetMapping("/{userId}")
public ResponseEntity<List<UserEvent>> getUserEvents(
@PathVariable String userId,
@RequestParam(required = false) LocalDateTime startDate,
@RequestParam(required = false) LocalDateTime endDate) {
List<UserEvent> events;
if (startDate != null && endDate != null) {
events = eventService.getUserEventsByPeriod(userId, startDate, endDate);
} else {
events = eventService.getUserEvents(userId);
}
return ResponseEntity.ok(events);
}
@GetMapping("/{userId}/statistics")
public ResponseEntity<Map<String, Long>> getEventStats(
@PathVariable String userId) {
return ResponseEntity.ok(eventService.getEventStatistics(userId));
}
}
6. Индексирование
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.index.CompoundIndex;
@Document(collection = "user_events")
@CompoundIndex(name = "user_timestamp_idx",
def = "{ 'userId': 1, 'timestamp': -1 }")
public class UserEvent {
@Id
private String id;
@Indexed
private String userId; // Отдельный индекс
@Indexed
private String sessionId;
private String eventType;
// ...
}
7. Unit тесты с Embedded MongoDB
import de.flapdoodle.embed.mongo.spring.autoconfigure.EmbeddedMongoAutoConfiguration;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = {Application.class, EmbeddedMongoAutoConfiguration.class}
)
public class EventServiceTest {
@Autowired
private EventService eventService;
@Autowired
private UserEventRepository eventRepository;
@BeforeEach
public void setUp() {
eventRepository.deleteAll();
}
@Test
public void testRecordEvent() {
String userId = "user123";
String eventType = "page_view";
Map<String, Object> data = Map.of("page", "/products");
UserEvent event = eventService.recordEvent(userId, eventType, data);
assertNotNull(event.getId());
assertEquals(userId, event.getUserId());
assertEquals(eventType, event.getEventType());
}
@Test
public void testGetEventStatistics() {
String userId = "user123";
eventService.recordEvent(userId, "page_view", Map.of());
eventService.recordEvent(userId, "page_view", Map.of());
eventService.recordEvent(userId, "click", Map.of());
Map<String, Long> stats = eventService.getEventStatistics(userId);
assertEquals(2, stats.get("page_view"));
assertEquals(1, stats.get("click"));
}
}
8. Проблемы, которые мы решали
Проблема 1: N+1 Query
// ❌ Плохо
List<User> users = userRepository.findAll();
for (User user : users) {
List<UserEvent> events = eventRepository.findByUserId(user.getId());
// N запросов!
}
// ✅ Хорошо
List<User> users = userRepository.findAllWithEvents();
Проблема 2: Размер документа
// ❌ Плохо — document может вырасти до лимита 16MB
@Document
public class User {
private List<UserEvent> allEvents; // Может быть очень много
}
// ✅ Хорошо — отдельная коллекция
@Document(collection = "users")
public class User {
private String id;
// ...
}
@Document(collection = "user_events")
public class UserEvent {
private String userId; // Foreign key
// ...
}
Проблема 3: Транзакции
// MongoDB 4.0+ поддерживает транзакции
@Transactional
public void transferData(String fromUser, String toUser) {
// Atomically!
eventService.removeUserEvents(fromUser);
eventService.addUserEvents(toUser);
}
Ключевые lessons learned
- Используй индексы для частых запросов
- Избегай очень больших документов (16MB лимит)
- Нормализуй данные если много связей
- Проверяй план агрегации с
.explain() - Тестируй с реальным MongoDB, не просто with in-memory
- Мониторь размер коллекций — документы растут
- Используй TTL индексы для автоматического удаления старых данных