DEV Community

Cover image for From MySQL to MongoDB in Spring Boot — Everything That Changed in My Code
Sanjay Kumar A
Sanjay Kumar A

Posted on

From MySQL to MongoDB in Spring Boot — Everything That Changed in My Code

In my last post I wrote about an error that cost me a full evening: my pom.xml had the MongoDB starter, but my code was still full of JPA annotations. The compiler kept saying cannot find symbol: class Entity.

That post was about the error. This post is about the fix — every single line I had to change to move my Task Manager project from MySQL to MongoDB.

If you are planning the same switch, this is the checklist I wish I had.


1. The dependency

Before (MySQL + JPA):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
Enter fullscreen mode Exit fullscreen mode

After (MongoDB):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

One starter replaces two dependencies. And this is exactly where my problem started — I added the new one but never removed the old one, so half my code still compiled and half did not.

Remove the JPA starter completely. If you leave it in, the jakarta.persistence annotations still resolve, and you will not notice you are mixing two worlds until something breaks at runtime.


2. application.properties

Before:

spring.datasource.url=jdbc:mysql://localhost:3306/taskmanager
spring.datasource.username=root
spring.datasource.password=yourpassword
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
Enter fullscreen mode Exit fullscreen mode

After:

spring.data.mongodb.uri=mongodb://localhost:27017/taskmanager
Enter fullscreen mode Exit fullscreen mode

Five lines became one.

No ddl-auto because MongoDB has no schema to create. No dialect because there is no SQL being generated. The database and the collection are created automatically the first time you insert a document.


3. The model class

This is where most of the work was. Here is my actual Task class after the migration:

package com.taskmanager.task_manager;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "app_tasks")
public class Task {

    @Id
    private String id;

    private String title;
    private String description;
    private String status = "PENDING";

    @DBRef
    @JsonIgnore
    private User user;

    public String getId() { return id; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getDescription() { return description; }
    public void setDescription(String desc) { this.description = desc; }
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
    public User getUser() { return user; }
    public void setUser(User user) { this.user = user; }
}
Enter fullscreen mode Exit fullscreen mode

Compare that with the JPA version it replaced:

@Entity
@Table(name = "app_tasks")
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String description;
    private String status = "PENDING";

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    // getters and setters
}
Enter fullscreen mode Exit fullscreen mode

The changes, one by one:

JPA MongoDB
@Entity @Document(collection = "app_tasks")
@Table(name = "...") folded into @Document
@Id from jakarta.persistence @Id from org.springframework.data.annotation
@GeneratedValue not needed
Long id String id
@ManyToOne + @JoinColumn @DBRef

The import trap

@Id exists in both worlds. The annotation name is identical, so your IDE will happily auto-import the wrong one and nothing will look wrong in the editor.

If you import jakarta.persistence.Id in a MongoDB project, Spring Data does not recognise it as the document ID. Your documents get saved with a generated _id that your Java field never sees, and findById() starts returning empty for records you know exist.

Check the import line. Not the annotation.

Why the ID became a String

MongoDB generates an ObjectId — a 24-character hex value like 66b3f1c2a4e5d67890abcd12. It does not fit in a Long.

You can map it to a String and let Spring Data convert it, which is what I did. That one change then ripples through your whole project, which brings me to the part that actually broke things.


4. The repository

Before:

public interface TaskRepository extends JpaRepository<Task, Long> {
    List<Task> findByStatus(String status);
}
Enter fullscreen mode Exit fullscreen mode

After:

public interface TaskRepository extends MongoRepository<Task, String> {
    List<Task> findByStatus(String status);
}
Enter fullscreen mode Exit fullscreen mode

Two changes: the interface name and the ID type. The derived query method findByStatus works exactly the same — Spring Data reads the method name and builds a Mongo query instead of SQL.

This was the nicest surprise of the migration. If your repository only uses derived query methods, it migrates almost for free.


5. What actually broke

The model and repository were easy. These were not.

Every ID in the controller

Long id became String id in the entity, so every method signature that touched an ID had to change too:

// Before
@GetMapping("/{id}")
public Task getTask(@PathVariable Long id) { ... }

// After
@GetMapping("/{id}")
public Task getTask(@PathVariable String id) { ... }
Enter fullscreen mode Exit fullscreen mode

The compiler catches most of these. What it does not catch is any place you were doing arithmetic or comparison on the ID, or parsing it with Long.parseLong(). Search your project for parseLong before you assume you are done.

@DBRef is not a join

I replaced @ManyToOne with @DBRef, and it does store a reference to the User document. But it is not a SQL join.

MongoDB fetches the referenced document with a second query. Load 50 tasks with @DBRef users and you get 51 database round trips. In SQL, one join would have done it.

For a student project with a small dataset this is fine. If your data is going to grow, the MongoDB way is to embed the fields you actually need — for example, storing just userId and username directly on the task — instead of referencing a whole document.

I kept @DBRef because my task list is per-user and never loads more than a handful at a time. Know that you are making that trade, though.

Native queries

Anything written as @Query(value = "SELECT * FROM ...", nativeQuery = true) is gone. There is no SQL to run. You rewrite those either as derived method names or as MongoDB's own query syntax:

@Query("{ 'status': ?0 }")
List<Task> findByStatusCustom(String status);
Enter fullscreen mode Exit fullscreen mode

Same annotation name, completely different language inside it.

Transactions

@Transactional across multiple saves does not behave the way you expect out of the box. MongoDB supports multi-document transactions, but only on a replica set — a plain standalone local install will not give them to you.

I did not need them, so I moved on. Just do not assume the annotation is doing what it did before.


6. What I would do differently

Pick the database before writing the entity classes.

That sounds obvious. But what actually happened was: I followed a JPA tutorial, wrote my model, then read that MongoDB was "easier for flexible data" and swapped the dependency without touching anything else. The mismatch between my pom.xml and my code is what generated that cannot find symbol error in the first place.

The second thing: do not migrate a database because it sounds better. Ask what your data actually looks like. My tasks have a fixed set of fields and a clear relationship to a user — honestly, that is relational data, and MySQL was a reasonable fit for it.

I stayed on MongoDB because I wanted to learn it and because the flexible schema helps while I am still changing fields every week. That is a real reason. "It's more modern" is not.


The short checklist

  1. Remove the JPA starter and the SQL driver. Completely.
  2. Add spring-boot-starter-data-mongodb.
  3. Replace the datasource properties with spring.data.mongodb.uri.
  4. @Entity@Document, and check the @Id import.
  5. Long idString id, then fix every controller signature.
  6. JpaRepository<T, Long>MongoRepository<T, String>.
  7. Rewrite native SQL queries.
  8. Decide whether references or embedding fits your data.

If you have made this switch, I want to know one thing: did you go with @DBRef or did you embed? I am still not sure I picked right.

Top comments (0)