尝试更新数据库时丢失数据

时间:2017-11-22 00:34:30

标签: java spring hibernate spring-data thymeleaf

我正在编写简单的博客网络应用程序。 我想要做的是从数据库加载数据,将其传递给表单输入(不是全部,只有我想要编辑的那些)编辑它们并将它们作为更新传递给数据库。

我正在从数据库加载我的帖子,数据正确传递给编辑表单,甚至deugger看到Post已经设置了作者,但是在提交此表单后,我得到错误,当user_id为null时我无法保存实体。为什么呢?

我尝试了什么?

  • 通过在存储库中编写我自己的提取查询来省略默认的hibernate的lazyFetching
  • 使用athuor的用户名添加不可编辑的文本字段以编辑表单

我的代码:

发布模型:

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;
    }
}

用户模型:

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;
    }
}

邮政控制人:

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";
    }
}

发布存储库:

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);
}

邮政服务:

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;
    }
}

修改表单视图:

<!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>

浏览器中返回错误:

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

来自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]

2 个答案:

答案 0 :(得分:1)

您的Usernull,或者它未被保留,只留下id null

确保您的User来自数据库,在存储Post之前将其存储在数据库中,或确保将Post cascades存储到{{1}实体。

根据您自己的答案和评论中的调试信息进行更新

看起来您正在尝试将当前用户保留在会话中并将其用作起点,然后从请求中包含的字段进行更新。

错误在于模型默认只有请求范围。 一旦邮件请求到达,您在get请求期间放置的User就会消失。

您应该能够通过向控制器添加Post来修复该行为,这会使属性会话成为范围,因此当您返回时它仍然存在。

您可以详细了解@SessionAttributes("post")如何运作以及如何在这个出色的答案中使用它:https://stackoverflow.com/a/26916920/66686

答案 1 :(得分:0)

在使用调试器分析程序流时,我推断提交表单会创建整个新的Post对象,其中包含来自视图表单输入字段的数据。

我是怎么找到的?创建日期始终设置为编辑时间。传递给提交表单的编辑帖子也只设置了标题和内容,这是我表单中唯一的输入字段。

如果传递给提交方法的帖子是全新的对象,则无法接收作者的ID,因为我只是在添加帖子表单时设置它,我收到活动登录用户并将其设置为帖子的作者。

我找到了解决此问题的方法。

@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";
    }

我在这里做的是接收来自Post对象的编辑数据,这是Post的Title和Contents,我从路径变量接收原始Post的ID。然后我从数据库再次加载此Post并在表单中的那些已编辑变量上设置其标题和内容字段。 这意味着Post设置了作者的ID,创建日期保持不变,并且更改的只是我想要编辑的字段:标题和内容。

也许这不是优雅的解决方法,它应该可以工作而无需从数据库加载Post,只需通过接收此帖子 观点,但它没有奏效。

我正在寻找真正解决这个问题,但现在它工作正常。