Post-MongoDB Migration: Spring Data and Hibernate Need Per-Method Regression, Not Framework Names
When migrating from MongoDB to KingbaseES, the amount of code changes is often not determined by the number of collections, but by which MongoDB APIs the application actually uses. Repository derived queries, aggregation pipelines, batch writes, transaction sessions, and index management each have different verification scopes; driver connectivity only confirms that the connection layer is established.
A Java service often has more than one data access method. Document interfaces continue with Spring Data MongoDB, order and user entities are managed by Hibernate, and operational reports need SQL to read document fields. Listing framework names in the technology stack cannot replace item-by-item regression. Kingbase's MongoDB smooth migration solution introduces compatibility and migration paths; when applied to an application, you still need to check the combination of current driver, authentication, called APIs, and target version.
Find Real Calls from Code
Migration review can start from usage points of Repository and MongoTemplate. Derived queries may look simple, but they can also include sorting, pagination, null handling, and nested field paths. JSON fragments in @Query, aggregation pipelines, batch updates, and transaction sessions must be registered separately; one "basic CRUD passed" cannot replace them.
For example, the following Repository only covers finding by SKU and status. Whether pagination is stable also depends on whether the sort field uniquely determines the result order; if only paginating by status, the same record may appear repeatedly on adjacent pages.
public interface CatalogRepository
extends MongoRepository<CatalogDocument, String> {
Optional<CatalogDocument> findBySku(String sku);
Page<CatalogDocument> findByPublishState(
String publishState, Pageable pageable);
}
Pageable query regression samples should fix the total count, sort field, boundary pages, and empty results. If the business requires ordering by update time descending, it is recommended to use SKU as the secondary sort key:
Pageable page = PageRequest.of(
pageNumber,
pageSize,
Sort.by(Sort.Order.desc("updatedAt"),
Sort.Order.asc("sku")));
Page<CatalogDocument> result = catalogRepository
.findByPublishState("PUBLISHED", page);
Another common entry point is MongoTemplate. It usually handles dynamic conditions, update statements, and aggregation queries, and can be easily missed during code search. The condition below consists of tenant, status, and nested attributes. Tests should separately cover documents with fields present, fields missing, empty arrays, and mismatched numeric types:
Query query = Query.query(
Criteria.where("tenantId").is(tenantId)
.and("publishState").is("PUBLISHED")
.and("attributes.warranty.months").gte(24))
.with(Sort.by(Sort.Order.asc("sku")))
.limit(200);
List<CatalogDocument> rows = mongoTemplate.find(
query, CatalogDocument.class, "catalog");
Such a call list is more useful than the words "Spring Data supported". Before migration, you can record per method the collection, operation type, whether transactions are used, input boundaries, and expected results. Slow queries in application logs should also be included; low-frequency month-end batch processes may not appear in daily integration traffic.
Spring Data Preserves Document Access Habits
Spring Data MongoDB's Repository and template interfaces let existing services continue to work with documents using the familiar object model. Connection configuration must still match the target service address, authentication database, and TLS requirements; account passwords must not be hardcoded in source or logs; configuration injection should follow the project's secret management mechanism. After connection succeeds, first verify read/write, update conditions, delete ranges, and exception returns, then run business-layer tests.
Updates especially need attention to the match scope. The example below updates one field based on SKU only; after the call, check the matched count and modified count; if the business requires "create if not found", you must separately verify upsert behavior and not assume that a successful ordinary update also means insertion is correct.
Query filter = Query.query(
Criteria.where("tenantId").is(tenantId)
.and("sku").is(sku));
Update update = new Update()
.set("publishState", "PUBLISHED")
.currentDate("updatedAt");
UpdateResult result = mongoTemplate.updateFirst(
filter, update, CatalogDocument.class, "catalog");
if (result.getMatchedCount() != 1) {
throw new IllegalStateException("catalog record not found");
}
Array updates, regex conditions, aggregation pipelines, and batch writes must be verified according to actual application usage. Document databases have broad expressiveness; a compatible framework does not mean all operators, driver versions, and parameter combinations are automatically consistent. Test samples should come from code call sites, not just a hand-written set of find and save for smoke testing.
Hibernate Manages Relational Entities, Transaction Boundaries in Service Layer
Hibernate is suitable for continuing to handle entities with clear structure and relational constraints. Entity classes map to tables on the target side; foreign keys, unique constraints, and transactions are still managed by the relational database connection. The entity example below only contains order master data and does not stuff every nested attribute from the document into Java fields:
@Entity
@Table(name = "sales_order")
public class SalesOrder {
@Id
@Column(name = "order_id")
private Long orderId;
@Column(name = "customer_id", nullable = false)
private Long customerId;
@Column(name = "order_status", nullable = false, length = 20)
private String orderStatus;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
}
When order and document status need to change together, first confirm the transaction manager actually used by the two frameworks. A @Transactional annotation usually only covers the transaction manager it is bound to; you cannot assume that just because both connection addresses belong to the same database product, the two client APIs are already part of the same atomic transaction.
@Transactional(transactionManager = "relationalTxManager")
public void createOrder(SalesOrder order) {
orderRepository.save(order);
outboxRepository.save(new OutboxEvent(
order.getOrderId(), "ORDER_CREATED"));
}
If a document write failure affects the order process, you can first write the pending event into the outbox of the relational transaction, have a separate consumer complete the document-side update, and then record the processing status. The consumer must handle events idempotently by event number; on failure, keep retry count, error summary, and last attempt time. This way failure recovery is traceable, and the boundary between two transaction managers is not hidden inside a service method.
SQL Handles Another Kind of Read, Not Replacing Framework Regression
Operational analysis occasionally needs to read document fields directly from SQL, for example filtering products by nested attributes. The document storage structure can be kept in a JSON column, with SQL/JSON functions reading values from paths. The following fields and data are a minimal model for demonstration; the specific syntax of JSON_VALUE must be confirmed according to the target version and compatibility mode:
CREATE TABLE catalog_document (
sku VARCHAR(40) PRIMARY KEY,
document_body JSON NOT NULL,
updated_at TIMESTAMP NOT NULL
);
SELECT sku,
JSON_VALUE(document_body, '$.attributes.color') AS color,
CAST(JSON_VALUE(document_body,
'$.attributes.warrantyMonths') AS INTEGER)
AS warranty_months
FROM catalog_document
WHERE CAST(JSON_VALUE(document_body,
'$.attributes.warrantyMonths') AS INTEGER) >= 24
ORDER BY updated_at DESC, sku;
Such SQL is suitable for reports, batch reconciliation, and data governance; it does not mean that all Java business calls must be changed to JDBC. The framework layer handles domain object writes and application rules, while SQL queries handle analysis tasks that require explicit filter criteria. Cases like mistaken paths, numbers stored as strings, and missing fields must have regression data; condition values should use parameter binding to avoid concatenating user input into SQL.
After migration, data verification can also use SQL for preliminary profile checks:
SELECT JSON_VALUE(document_body, '$.publishState') AS publish_state,
COUNT(*) AS document_count,
MAX(updated_at) AS latest_update
FROM catalog_document
GROUP BY JSON_VALUE(document_body, '$.publishState')
ORDER BY publish_state;
Total counts and group distributions can help detect missing batches, but they cannot prove that every nested array and date field is correct. When sampling, go back to the source document by business primary key, and compare the target JSON, framework deserialized object, and SQL value. For high-risk collections, add full primary-key verification, application regression, and data consistency checks during writes.
Acceptance Checklist Goes Down to Methods, Not Framework Names
An application's acceptance table can be split by operation type: Repository derived queries, template queries, updates and deletes, batch writes, transaction sessions, index operations, SQL/JSON queries. Each item records driver version, authentication method, target database version, sample input, and verification result. APIs not yet called by business code do not need to be tested all for "full functionality"; low-frequency branches currently in production also cannot be skipped just because they are rarely triggered.
During canary release, you can first do sampled comparison of query results, and for write operations check affected row counts and final status by business primary key. Logs should only keep necessary request identifiers, collection names, operation types, and durations, not credentials or complete sensitive documents. When differences are found, first fix a source record and a set of call parameters, then determine whether the cause is driver mapping, field type, API behavior, or a business code assumption.
Spring Data and Hibernate can take on different roles in the same application: the former maintains the document access model, the latter manages relational entities, and SQL/JSON queries handle clear document analysis needs. The criterion for successful switch remains that all paths actually called by the current service pass regression, not that dependencies have been replaced with compatible drivers.
Field Mapping Must Be Checked Against Business Values
When migrating documents, identical field names do not mean the Java object reads the same value. ObjectId, dates, Decimal128, arrays, and missing fields in MongoDB documents can all map to different types or default values in Java. Especially for amounts, precise times, and nullable fields, you cannot just compare whether the JSON text looks similar.
| Document field situation | Java-side check point | Target data check point |
|---|---|---|
| ObjectId or business primary key | string length, null handling, object references | primary key uniqueness, references still pointing to same object |
| Date and timezone | conversion rules for Instant, Date, LocalDateTime |
unified timezone, millisecond precision preserved |
| Decimal128 amount | BigDecimal precision and rounding strategy |
decimal places, boundary values, and summed amounts |
| Missing field vs explicit null | default values after deserialization | whether query conditions distinguish them |
| Array and nested objects | generics, empty arrays, unknown fields | array order, element count, and nested paths |
This table is not to turn every document field into a relational column, but to list the type semantics the application depends on. A source document without status and one with status: null may represent two business states; if the Java DTO initializes both to an empty string, some filter conditions will change after migration. Amount fields should be tested separately with maximum, minimum, and boundary decimal values, to prevent the database side having proper precision while the Java side first rounds with double.
Primary key strategy must also be clear. If the source uses ObjectId, when the target stores it as a string, preserve the reversible original identifier; if the application has a separate business code, confirm it is the cross-system association key. New identifiers generated during migration must not be mistakenly passed back to old interfaces as business keys; otherwise caches, messages, and external links will have records that cannot be matched.
Canary Reads Make Differences Easier to Locate Than One-Time Switch
Services that can double-read can pick a fixed small set of business requests, read from both source and target simultaneously, and compare business fields and sort results; for scenarios not suitable for double-write, replay the same request sample in the test environment. Comparison should include hit count, primary key set, total amounts, update times, and distribution of null fields. Comparing only total row counts can miss a problem where one record is lost and another is duplicated, and the counts happen to offset.
Target-side SQL can first do basic distribution verification:
SELECT COUNT(*) AS document_count,
COUNT(DISTINCT sku) AS distinct_sku_count,
MIN(updated_at) AS earliest_update,
MAX(updated_at) AS latest_update
FROM catalog_document;
SELECT JSON_VALUE(document_body, '$.publishState') AS publish_state,
COUNT(*) AS rows_in_state
FROM catalog_document
GROUP BY JSON_VALUE(document_body, '$.publishState')
ORDER BY publish_state;
If the total count and distinct primary key count are inconsistent, first check duplicate key handling; if state distribution differs, sample the source document, target JSON, and Java deserialized object. Query samples should cover empty fields, documents from historical versions, and recently updated records, avoiding only the most regularly structured common data.
During canary, also distinguish between reads and writes. Query traffic can be switched in batches by tenant or business key; before switching writes, confirm the final sync position of incremental data. When the original service still writes to the source, target-side results are only a snapshot at a moment in time; you need to record the incremental catch-up point in the switch window before stopping source-end writes or enabling a verified write strategy.
If a particular call still differs on the target side, during canary you only roll back that interface or tenant, and keep the source end receiving writes. Both ends receiving new writes simultaneously will introduce duplicates and conflicts into the next round of incremental reconciliation.
Separate Framework Upgrade and Database Migration Troubleshooting
If you upgrade Spring Boot, Spring Data, Hibernate, and JDBC drivers at the same time as the migration, it becomes difficult to attribute interface exceptions. When the schedule allows, first fix framework and driver versions to complete database compatibility regression, then upgrade the framework; if you must upgrade together, at least save the pre-upgrade dependency tree, connection configuration, and key Repository test results.
If the earlier MongoTemplate query returns results on the source but is empty on the target, replay with the same tenantId and sku. If results recover after removing the warranty.months condition, check the type of that field in the source document and target JSON; if still empty, check publishState and tenant conditions. After finding the specific difference, examine the corresponding driver request and target-side return value. For Hibernate entity mapping exceptions, check JDBC dialect, column types, and constraints. Record the input parameters during replay to ensure both ends are actually querying the same data.
Before production switch, rebuild the connection pool in a pre-release environment to confirm that new connections indeed point to the target; also replay month-end batch processing and exception compensation separately, as common interface integration may not cover these branches. The switch window is bounded by the incremental catch-up point; verify target documents and update times by business primary key, and only after confirming data is caught up remove the old connection configuration.