Search code examples
javaspringhibernatespring-datathymeleaf

Losing data while trying to update database


I am writing simple blog web app. What I want to do is load data from database, pass it to view into form inputs(not all, only those I want to be editable) edit them and pass them to database as update.

I am loading my Post from database, data correctly passes to edit form, even deugger see that Post has author setted, but after submitting this form, I got error that I cannot save entity while user_id is null. Why?

What I tried?

  • ommitting default hibernate's lazyFetching by writing my own fetching query in repository
  • adding non-editable text-field with athuor's username to edit form

My code:

Post model:

package io.gromo13.personalBlog.model;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Date;

@Entity
@Table(name = "posts")
public class Post {

    @Id
    @Column(name = "id", nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "title", nullable = false)
    @NotNull
    @Size(min = 3, max = 50)
    private String title;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User author;

    @Column(name = "date", nullable = false)
    private Date creationDate;

    @Column(name = "contents", nullable = false)
    @NotNull
    @Size(min = 3, max = 500)
    private String contents;

    public Post(String title, String contents, User author) {
        this();
        this.title = title;
        this.contents = contents;
        this.author = author;
    }

    public Post() {
        creationDate = new Date();
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public User getAuthor() {
        return author;
    }

    public void setAuthor(User author) {
        this.author = author;
    }

    public Date getCreationDate() {
        return creationDate;
    }

    public void setCreationDate(Date creationDate) {
        this.creationDate = creationDate;
    }

    public String getContents() {
        return contents;
    }

    public void setContents(String contents) {
        this.contents = contents;
    }
}

User model:

package io.gromo13.personalBlog.model;

import org.hibernate.validator.constraints.Email;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;

@Entity
@Table(name="users")
public class User {

    @Id
    @Column(name = "id", nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "username", nullable = false)
    @NotNull
    @Size(min=3, max=20)
    private String username;

    @Column(name = "password", nullable = false)
    @NotNull
    @Size(min=5, max=20)
    private String password;

    @Column(name = "email", nullable = false)
    @NotNull
    @Email
    private String email;

    @ManyToOne
    @JoinColumn(name = "role_id", nullable = false)
    @NotNull
    private Role role;

    @OneToMany(mappedBy = "author")
    private List<Post> posts;

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    public User() {

    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Role getRole() {
        return role;
    }

    public void setRole(Role role) {
        this.role = role;
    }

    public List<Post> getPosts() {
        return posts;
    }

    public void setPosts(List<Post> posts) {
        this.posts = posts;
    }
}

Post controller:

package io.gromo13.personalBlog.controller;

import io.gromo13.personalBlog.model.User;
import io.gromo13.personalBlog.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import io.gromo13.personalBlog.model.Post;
import io.gromo13.personalBlog.service.PostService;
import javax.validation.Valid;

@Controller
@RequestMapping("/post")
public class PostController {

    @Autowired
    private PostService postService;

    @Autowired
    private UserService userService;

    public void setPostService(PostService postService) {
        this.postService = postService;
    }

    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/add")
    public String addPost(Model model) {
        model.addAttribute("post", new Post());
        return "/post/add";
    }

    @PostMapping("/add")
    public String addPostSubmit(@Valid Post post, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "/post/add";
        }

        User user = getLoggedInUser();
        post.setAuthor(user);

        postService.add(post);

        return "redirect:/admin/posts";
    }

    private User getLoggedInUser() {
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (principal instanceof org.springframework.security.core.userdetails.User) {
            String username = ((org.springframework.security.core.userdetails.User) principal).getUsername();
            User user = userService.findByUsername(username);
            return user;
        }

        return null;
    }

    @GetMapping("/edit/{id}")
    public String editPost(@PathVariable Long id,  Model model) {
        Post post = postService.getEager(id);
        model.addAttribute("post", post);

        return "/post/edit";
    }

    @PostMapping("/edit/{id}")
    public String editPostSubmit(@ModelAttribute @Valid Post post, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "/post/edit";
        }
        postService.edit(post);

        return "redirect:/admin/posts";
    }
}

Post repository:

package io.gromo13.personalBlog.repository;

import io.gromo13.personalBlog.model.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface PostRepository extends JpaRepository<Post, Long> {
//    @Query("SELECT post FROM Post post INNER JOIN FETCH post.author AS author where post.id = :id and post.author = author.id")
    @Query("SELECT post from Post post inner join fetch post.author as author where post.id = :id and post.author = author.id")
    Post findWithAuthorById(@Param("id") Long id);
}

Post service:

package io.gromo13.personalBlog.service;

import io.gromo13.personalBlog.repository.PostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import io.gromo13.personalBlog.model.Post;

import java.util.List;

@Service
public class PostService {

    @Autowired
    private PostRepository postRepository;

    public void setPostRepository(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    public void add(Post post)  {
        postRepository.save(post);
    }

    public Post get(Long id) {
        return postRepository.findOne(id);
    }

    public Post getEager(Long id) {
        return postRepository.findWithAuthorById(id);
    }

    public List<Post> getAll() {
        return (List<Post>) postRepository.findAll();
    }

    public void edit(Post post) {
        postRepository.save(post);
    }

    public void delete(Long id) {
        postRepository.delete(id);
    }

    public List<Post> getLatestPosts(int id, int count) {
        //TODO implement
        return null;
    }
}

Edit form view:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Title</title>
</head>
<body>
    <div th:replace="/fragments/header"> </div>

    <div>
        <h3>Editing post</h3>
        <form id="editPostForm" action="#" th:action="@{/post/edit/} + ${post.id}" th:object="${post}" method="post">
            <!--<p>Author: <input type="text" th:field="*{author.username}" readonly="readonly" /></p>-->
            <p>Author: <span th:text="*{author.username}"></span></p>
            <p>Creation date: <span th:text="*{creationDate}"></span></p>
            <p>Title: <input type="text" th:field="*{title}" /></p>
            <p th:if="${#fields.hasErrors('title')}" th:errors="*{title}"></p>
            <p>Contents:</p>
            <textarea th:form="editPostForm" rows="5" cols="30" th:field="*{contents}"></textarea>
            <p th:if="${#fields.hasErrors('contents')}" th:errors="*{contents}"></p>
            <p><input type="submit" value="Submit" /><input type="reset" value="Reset" /></p>
        </form>
    </div>

    <div th:replace="/fragments/footer"> </div>
</body>
</html>

Error returned in browser:

Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.
Wed Nov 22 01:42:42 CET 2017
There was an unexpected error (type=Internal Server Error, status=500).
could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement

Exception + part of database logs from IDE:

 Hibernate: update posts set user_id=?, contents=?, date=?, title=? where id=?
2017-11-22 01:42:42.920  WARN 5192 --- [nio-8080-exec-7] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1048, SQLState: 23000
2017-11-22 01:42:42.920 ERROR 5192 --- [nio-8080-exec-7] o.h.engine.jdbc.spi.SqlExceptionHelper   : Column 'user_id' cannot be null
2017-11-22 01:42:42.922  INFO 5192 --- [nio-8080-exec-7] o.h.e.j.b.internal.AbstractBatchImpl     : HHH000010: On release of batch it still contained JDBC statements
2017-11-22 01:42:42.966 ERROR 5192 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement] with root cause

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'user_id' cannot be null
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_131]
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_131]
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_131]
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_131]
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) ~[mysql-connector-java-5.1.44.jar:5.1.44]
    at com.mysql.jdbc.Util.getInstance(Util.java:408) ~[mysql-connector-java-5.1.44.jar:5.1.44]
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:935) ~[mysql-connector-java-5.1.44.jar:5.1.44]
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3973) ~[mysql-connector-java-5.1.44.jar:5.1.44]
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3909) ~[mysql-connector-java-5.1.44.jar:5.1.44]
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2527) ~[mysql-connector-java-5.1.44.jar:5.1.44]
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2680) ~[mysql-connector-java-5.1.44.jar:5.1.44]
    at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2487) ~[mysql-connector-java-5.1.44.jar:5.1.44]
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1858) ~[mysql-connector-java-5.1.44.jar:5.1.44]
    at com.mysql.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2079) ~[mysql-connector-java-5.1.44.jar:5.1.44]
    at com.mysql.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2013) ~[mysql-connector-java-5.1.44.jar:5.1.44]
    at com.mysql.jdbc.PreparedStatement.executeLargeUpdate(PreparedStatement.java:5104) ~[mysql-connector-java-5.1.44.jar:5.1.44]
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1998) ~[mysql-connector-java-5.1.44.jar:5.1.44]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_131]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_131]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
    at org.apache.tomcat.jdbc.pool.StatementFacade$StatementProxy.invoke(StatementFacade.java:114) ~[tomcat-jdbc-8.5.23.jar:na]
    at com.sun.proxy.$Proxy112.executeUpdate(Unknown Source) ~[na:na]
    at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:204) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.engine.jdbc.batch.internal.NonBatchingBatch.addToBatch(NonBatchingBatch.java:45) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3134) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3013) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3393) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:145) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:582) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:456) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:337) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:39) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1282) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:465) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:2963) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2339) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:485) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:147) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$100(JdbcResourceLocalTransactionCoordinatorImpl.java:38) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:231) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:65) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:61) ~[hibernate-entitymanager-5.0.12.Final.jar:5.0.12.Final]
    at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:517) ~[spring-orm-4.3.12.RELEASE.jar:4.3.12.RELEASE]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:761) ~[spring-tx-4.3.12.RELEASE.jar:4.3.12.RELEASE]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:730) ~[spring-tx-4.3.12.RELEASE.jar:4.3.12.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:504) ~[spring-tx-4.3.12.RELEASE.jar:4.3.12.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:292) ~[spring-tx-4.3.12.RELEASE.jar:4.3.12.RELEASE]

Solution

  • Either your User is null or it is not persisted, leaving it's id null.

    Make sure your User is coming from the database, store it in the database before storing the Post or make sure storing the Post cascades to the User entity.

    Update based on your own answer and the debugging information in the comments

    It looks like you are trying to keep the current user in the session and use it as a starting point which then gets updated from the fields included in the request.

    The mistake is that the Model has by default only request scope. The Post you put there during the get request is gone once the post request arrives.

    You should be able to fix that behavior by adding @SessionAttributes("post") to your controller which makes the attribute session scoped, so it will still be there when you return.

    You can read more about how @ModelAttribute works and how it can be used in this excellent answer: https://stackoverflow.com/a/26916920/66686