1

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]
Gromo
  • 360
  • 2
  • 4
  • 13

2 Answers2

1

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

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
Jens Schauder
  • 77,657
  • 34
  • 181
  • 348
  • i think I am sure that User is comingfrom database, because I load Post from database and I am adding it to edit view to fill the form. In edit view I have simple line: `

    Author:

    ` which prints post's author's username in that view and it works. Something is wrong when I am clicking Submit in that form.
    – Gromo Nov 22 '17 at 14:15
  • can you print out the user id before saving? That should make it obvious if it is a persisted entity or not. – Jens Schauder Nov 22 '17 at 15:28
  • Actually no. I receive `NullPointerException`. Here is my whole debuggin process step by step how it looks: [Debugging prosess step by step on screenshots](https://imgur.com/a/2DGuW) – Gromo Nov 22 '17 at 15:44
  • updated my answer with a proposal how to fix the issue. – Jens Schauder Nov 22 '17 at 18:09
  • This answer workes well. Simple `@SesionAttributes("post")` as class annotation for PostController class and then, in submitting form method, simple `@ModelAttribute("post")` to receive post from session. Thank you very much. I learned a lot about Get and Post requests in Spring from your answer, – Gromo Nov 22 '17 at 18:25
0

While analysing program flow with debugger I deduced that submitting form creates entire new Post object filled with data from view's form's input fields.

How I found it? Creation date was always set for edition time. Also edited post passed to submit form had only set Title and Contents, which are the only input fields in my form.

If post passed to submit method was entire new object, it was unable to receive author's id, because I am setting it only in adding post form where I receive active logged in user and set him as author of post.

I found workaround for this problem.

@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("post") @Valid Post post, @PathVariable Long id, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "/post/edit";
        }

        Post editedPost = postService.getEager(id);
        editedPost.setTitle(post.getTitle());
        editedPost.setContents(post.getContents());
        postService.edit(editedPost);

        return "redirect:/admin/posts";
    }

What I am doing here is receiving edited data from passed Post object, which are Title and Contents of Post, I am receiving original Post's ID from path variable. Then i load once more this Post from database and set his title and contents fields on those edited variables in form. This means that Post have set author's id, creation date remains unchanged, and changed are only fields that I want to be editablle: title and contents.

Maybe it's not elegant workaround and it should work without loading Post from database, just by receiving this Post from view, but it didn't worked.

I am stil looking for true resolving this problem, but for now it works fine.

Gromo
  • 360
  • 2
  • 4
  • 13