← Назад к вопросам

Как взаимодействовал с MongoDB

2.0 Middle🔥 201 комментариев
#Кэширование и NoSQL

Комментарии (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

  1. Используй индексы для частых запросов
  2. Избегай очень больших документов (16MB лимит)
  3. Нормализуй данные если много связей
  4. Проверяй план агрегации с .explain()
  5. Тестируй с реальным MongoDB, не просто with in-memory
  6. Мониторь размер коллекций — документы растут
  7. Используй TTL индексы для автоматического удаления старых данных