Skip to content

lidimayra/from-rails-to-spring-boot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 

Repository files navigation

From Rails to Spring Boot

Like Rails, Spring Boot also follows Convention over Configuration principles. This repository's goal is to focus on similarities and differences between both frameworks in order to provide a quick guide for developers that are migrating from one to another.

Contributions are welcome!

Pre-requisite
Maven instalation
Spring Boot instalation
App Initialization
Controllers & Views
Project Structure
RESTful routes
From Rails Models to Spring Entities
Performing a creation through a web interface
Displaying a collection of data
Editing and Updating data
Showing a Resource
Destroying a Resource

Pre-requisite

Java Development Kit 8

Maven instalation

On Ubuntu

sudo apt update
sudo apt install maven

On Mac OS (with Homebrew)

brew update
brew install maven

Spring Boot CLI instalation

On Ubuntu (with SDKMAN)

curl "https://get.sdkman.io" | bash
source ~/.sdkman/bin/sdkman-init.sh
sdk install springboot

On Mac (with Homebrew)

brew tap pivotal/tap
brew install springboot

App Initialization

Once Spring Boot CLI is installed, we can use spring init command to a start a new Spring Boot project (just like we would do with rails new):

# rails new <app_name>
spring init <app_name> -d=web,data-jpa,h2,thymeleaf

-d allows us to specify dependencies we want to set up. In this example we're using the ones that are aimed at a basic web project:

  • web: Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.
  • data-jpa: Persist data in SQL stores with Java Persistence API using Spring Data and Hibernate.
  • h2: Provides a fast in-memory database that supports JDBC API, with a small (2mb) footprint. Supports embedded and server modes as well as a browser based console application.
  • thymeleaf: Server-side Java template engine

Example of Spring Boot initialization.

Note that a class was created named as DemoApplication.java in src/main/java/com/example/<app_name>/ (Example)

By default, Spring uses Maven as the project management tool. After running the command above, dependencies can be found in pom.xml file, at the root directory.

Install dependencies specified in pom.xml by using Maven:

# bundle install
mvn clean install

Start the server using spring-boot:run, a task that's provided by Maven plugin:

# rails s
mvn spring-boot:run

Now application can be accessed at http://localhost:8080/. At this point, an error page will be rendered, as there are no controllers defined so far.

Controllers and views

In Spring Boot, there is no such thing as the rails generators. Also, there is no file like routes.rb, where all routes are specified in a single place.

Write the controller inside <app_name>/src/main/java/<package_name>:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class FooController {
    @GetMapping("/foo")
    public String index() {
        return "bar";
    }
}

The @GetMapping annotation ensures that GET requests performed to /foo will be mapped to the method declared right after it (there is no file similar to Rails' routes.rb in Spring Boot. Routes are defined alongside with its methods).

Because of Thymeleaf, by returning the String "bar", the application will look for an HTML file of the same name in src/main/resources/templates/

Create the following page: bar.html

<p>FooBar</p>

Example

Now, if we run the application with mvn spring-boot:run command and access it at http://localhost:8080/foo, we'll see the bar.html page being rendered.

Project Structure

At this point, we have the initial structure of a Maven project.

In the root directory, we have the pom file: pom.xml. This is the Maven build specification. Like in Rails Gemfile, it contains the project's dependencies declarations.

RESTful routes

Let's say we want to build a blog containing the seven RESTful actions (index, new, create, show, edit and destroy) for posts path. In Rails, we could achieve that by defining resources: :posts in routes.rb file.

As mentioned previously, Spring Boot does not have a central point where all routes are specified. Those are defined in the controllers instead.

We've already seen an example using @GetMapping annotation to demonstrate the definition of a route that uses GET method. Similarly, Spring supports other four inbuilt annotations for handling different types of HTTP request methods: @PostMapping, @PutMapping, @DeleteMapping and @PatchMapping.

Example of these concepts being applied for the blog posts can be found in here.

From Rails Models to Spring Entities

In order to represent a Post in the application-level, we'll need to define it as an Spring JPA Entity (very similar to the way it would be done with a Model in Rails).

@Entity // Designate it as a JPA Entity
public class Post {

    @Id // Mark id field as the entity's identity
    @GeneratedValue(strategy = GenerationType.AUTO) // Value will be automatically provided
    private Long id;
    private String title;
    private String content;

    public Long getId() { ... }

    public void setId(Long id) { ... }

    public String getTitle() { ... }

    public void setTitle(String title) { ... }

    public String getContent() { ... }

    public void setContent(String content) { ... }
}

Spring Data JPA provides some built-in methods to manipulate common data persistence operations through the usage of repositories in a way that's very similar to Rails' ActiveRecord. So, to work with Post data, a PostRepository must be implemented as well:

public interface PostRepository extends JpaRepository<Post, Long> {
}

JpaRepository interface takes to params, in this scenario: Post and Long. Post because it is the entity that will be used and Long because that's the type of Post's identity (ID).

This interface will be automatically implemented at runtime.

Whole example can be found in here.

Performing a creation through a web interface

Next step is adding a form to submit posts to the blog. At this point, we already have the templates/blog/new.html file containing a single line in it.

Using Thymelaf, we can do that with the following approach:

<!DOCTYPE html SYSTEM
"http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <body>
        <p>New Post</p>
        <form method="POST" action="/posts">
            <label for="title">Title:</label>
            <input type="text" name="title" size="50"></input><br/>
            <label for="content">Content:</label><br/>
            <textarea name="content" cols="80" rows="5"></textarea>
            <br/>
            <input type="submit"></input>
        </form>
    </body>
</html>

And then, BlogController must be adjusted to permit that when a POST request to /posts is performed, the submitted params must be used to create this new post.

@Controller
public class BlogController {

    @Autowired
    private PostRepository postRepository;

    @GetMapping("/posts")
    public String listPosts() { ... }

    @PostMapping("/posts")
    public String createPost(Post post) {
        postRepository.save(post); // Use JPA repository built-in method.
        return "redirect:/posts"; // redirect user to /posts page.
    }
}

See whole implementation in here.

Displaying a collection of data

We'll make changes to /posts page so it will list all posts that are recorded in the database.

BlogController's method that's associated to this route needs to be adjusted for making this data available to the view:

@GetMapping("/posts")
public String listPosts(Model model) {
    List<Post> posts = postRepository.findAll();
    model.addAttribute("posts", posts);
    return "blog/index";
}

In Spring, Models are used to hold application data and make it available to the view (like instance variables in Rails). In this example, we're adding the list of posts to a key named posts, so we can access it from the template.

Following code must be implemented to templates/blog/index.html:

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">

<h1>Blog</h1>

<dl th:each="post : ${posts}">
    <dt>
        <span th:text="${post.title}">Title</span>
    </dt>

    <dd>
        <span th:text="${post.content}">Content</span>
    </dd>
</dl>

<a th:href="@{/posts/new}">Submit a new post</a>

See implementation in here.

Now, accessing application at http://localhost:8080/posts, it is possible to list and to submit posts using the features implemented so far. Similar approach can be applied to implement the other actions.

Editing and Updating data

Now we want to enable editing/updating functionalities. Following changes must be made to editPost() method in BlogController:

@getMapping("/posts/{postId}/edit")
public String editPost(@PathVariable("postId") long id, Model model) {
    Post post = postRepository.findById(id)
          .orElseThrow(() -> new IllegalArgumentException("Invalid Post
Id:" + id)); // Ensure post exists before rendering edit form

    model.addAttribute("post", post); // enable post to be consumed by edit template

    return "blog/edit"; // render edit template
}

Note that the id parameter contains a @PathVariable annotation. This annotation indicates that this param must receive a value that's embedded in the path. In this case, id param will have the value that's passed as postId when performing a request to /posts/{postId}/edit. Just like we would do by calling params[postId] in Rails.

Then, we must implement the edit form:

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <body>
        <p>Edit Post</p>
        <form th:method="post"
              th:action="@{/posts/{id}(id=${post.id})}"
              th:object="${post}">

            <input type="hidden" name="_method" value="patch" />
            <label for="title">Title:</label>
            <input type="text" name="title" size="50" th:field="${post.title}"></input>
            <br/>
            <label for="content">Content:</label>
            <br/>
            <textarea name="content" cols="80" rows="5" th:field="${post.content}"></textarea>
            <br/>
            <input type="submit"></input>
        </form>
    </body>
</html>

This is enough to render an edit form. Thanks to Thymeleaf we can use th:field to map Post fields and provide a pre-populated form to the final user. At this point, edit form can be accessed at https://localhost:8080/posts/<post_id>/edit.

However, as the update behavior wasn's implemented yet, it is still pointless to submit this form.

In order to implement it, the following changes are required in the BlogController:

@PatchMapping("/posts/{postId}")
public String updatePost(@PathVariable("postId") long id, Model model, Post post) {
    Post recordedPost = postRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("Invalid Post Id:" + id));

    recordedPost.setTitle(post.getTitle());
    recordedPost.setContent(post.getContent());
    postRepository.save(recordedPost);

    model.addAttribute("posts", postRepository.findAll());
    return "blog/index";
}

After these changes, posts are ready to be edited through the UI. An edit link can also be added to posts/index to enable edit form to be easily accessed:

<a th:href="@{/posts/{id}/edit(id=${post.id})}">Edit</a>

This implementation can be seen in here.

Showing a Resource

Given what've done so far, there is nothing new in implementing the feature responsible for showing a resource.

Changes to be performed to the controller:

@GetMapping("/posts/{postId}")
public String showPost() {
public String showPost(@PathVariable("postId") long id, Model model) {
    Post post = postRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("Invalid Post Id:" + id));

    model.addAttribute("post", post);

    return "blog/show";
}

And a simple template to display title and content for a single post:

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">

    <body>
        <h1 th:text="${post.title}"></h1>
        <hr>
        <p th:text="${post.content}"></p>

        <p><a th:href="@{/posts/{id}/edit(id=${post.id})}">Edit</a></p>
        <hr>
        <a th:href="@{/posts/}">Go back to posts</a>
    </body>
</html>

These changes enable post details to be available at https://localhost:8080/posts/<post_id>.

We can also add a link at posts index to allow direct access to show:

<a th:href="@{/posts/{id}/(id=${post.id})}">Show</a>

Implementation can be seen in here.

Destroying a Resource

Now, we'll add the feature to remove a post.

In BlogController:

@GetMapping("/posts/{postId}/delete")
public String deletePost(@PathVariable("postId") long id, Model model) {
    Post recordedPost = postRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("Invalid Post Id:" + id));

    postRepository.delete(recordedPost);
    model.addAttribute("posts", postRepository.findAll());
    return "blog/index";
}

Note that we're using GET method in here. That's because in this example, our app is a monolith and DELETE method is not supported by the browsers. In order to keep things simple and avoid the addition of a form with a hidden field to handle this method (like we did when updating), this one is being used as a GET. If this was an API, @DeleteMapping would be the ideal option.

And then we can add a link to delete in index page:

<a th:href="@{/posts/{id}/delete(id=${post.id})}">Delete</a>

Now it is possible to access https://localhost:8080/posts and delete each post by using the delete link that's displayed below it.

Implementation can be found in here.

About

A quick guide for developers migrating from Rails to Spring Boot with examples

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published