Kaskadowy zapis dzieci z kluczem rodzica

0

Na wstępie uprzedzam że nie jestem dobrze obeznany z Spring i JPA/Hibernate dopiero się wdrażam, więc mój kod może być "brzydki architektonicznie" nie mam opanowanego wzorca budowy aplikacji dla Spring :) . W kontrolerze otrzymuję zmapowany z JSONa obiekt. Który chcę zapisać do bazy. Fragment który chcę zapisać wygląda w ten sposób:

REATE TABLE IF NOT EXISTS `journal_entry` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `vehicle_journal` bigint(20) NOT NULL,
  `cost` int(10) unsigned DEFAULT NULL,
  `type` tinyint(3) unsigned DEFAULT NULL,
  `name` varchar(25) DEFAULT NULL,
  `fuel_amount_added` float DEFAULT NULL,
  `unit_price` int(11) DEFAULT NULL,
  `fuel_type` tinyint(4) DEFAULT NULL,
  `full_filled` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_journal_entry_type` (`type`),
  KEY `fk_ft` (`fuel_type`),
  CONSTRAINT `fk_ft` FOREIGN KEY (`fuel_type`) REFERENCES `fuel_type` (`id`),
  CONSTRAINT `fk_vehicle_journal` FOREIGN KEY (`vehicle_journal_id`) REFERENCES `vehicle_journal` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `vehicle_journal` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `vehicle` bigint(20) NOT NULL,
  `created` timestamp NOT NULL DEFAULT current_timestamp(),
  `updated` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(),
  `milage` int(10) unsigned DEFAULT NULL,
  `description` varchar(255) DEFAULT NULL,
  `entry_time` timestamp NULL DEFAULT NULL,
  `place` varchar(25) DEFAULT NULL,
  `total_cost` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_vehicle` (`vehicle`),
  CONSTRAINT `fk_vehicle` FOREIGN KEY (`vehicle`) REFERENCES `vehicle` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

A w kodzie wygląda to tak:

@Table
@Entity
public class JournalEntry {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private Integer cost;
	
	@ManyToOne(fetch=FetchType.EAGER, optional=false)
	@JoinColumn(name="vehicle_journal", referencedColumnName="id", nullable=false)
	private VehicleJournal vehicleJournal;
	
	@Size(max=25)
	private String name;
	
	private Float fuelAmountAdded;
	
	private Integer unitPrice;
	
	@ManyToOne
	@JoinColumn(name="fuel_type", referencedColumnName="id")
	private FuelType fuelType;
	
	@Column(name="full_filled")
	private Boolean fullfiled;
//...

@Table
@Entity
public class VehicleJournal {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	@ManyToOne
	@JoinColumn(name="vehicle", referencedColumnName="id")
	private Vehicle vehicle;
	
	@DateTimeFormat
	@CreationTimestamp
	private LocalDateTime created;
	
	@DateTimeFormat
	@UpdateTimestamp
	private LocalDateTime updated;

	private Integer milage;
	
	@Size(max=255)
	private String description;
	
	@DateTimeFormat
	private LocalDateTime entryTime;
	
	@Size(max=25)
	private String place;
	
	private Integer totalCost;
	
	@OneToMany(mappedBy="vehicleJournal", cascade= { CascadeType.ALL, CascadeType.PERSIST}, fetch=FetchType.LAZY)
	private Set<JournalEntry> journalEntries = new HashSet<>(0);

//Fragment kontrolera

	@RequestMapping(path = "/create", method = RequestMethod.POST, consumes=MediaType.APPLICATION_JSON_VALUE)
	@PreAuthorize("isAuthenticated()")
	public ResponseEntity<?> create(Principal principal, @RequestBody VehicleJournal vehicleJournal) 
			throws JsonProcessingException{
		logger.debug("Adding new journal entry:" + jsonCustomMapper.writeValueAsString(vehicleJournal));
		
		//TODO:Add validation
		VehicleJournal savedVehicleJournal = vehicleJournalRepo.save(vehicleJournal); //interface VehicleJournalRepo extends CrudRepository<VehicleJournal, Long>
		
		if(savedVehicleJournal==null) {
			return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
		}else {
			return new ResponseEntity<VehicleJournal>(savedVehicleJournal, HttpStatus.ACCEPTED);
		}
	}


Pobieranie obiektów z bazy nie stanowi problemu, problem występuje tylko podczas zapisu. JPA/Hibernate wyrzuca błąd podczas zapisu obiektu dziecka journalEntry ponieważ pole rodzica vehicleJournal jest wymagane a JPA/Hibernate ma tam wartość null i baza odrzuca zapis. JPA/Hibernate powinien otworzyć transakcję zapisać rodzica następnie pobrać jego id i umieścić u dzieci. Pytanie czy mam sknocone adnotacje i powiązania czy nie mam wyjścia i muszę ręcznie utworzyć transakcję i zapisywać w odpowiedniej kolejności?

0

Pokaż logi błędu.

0

Log:

2019-05-09 1108.158 DEBUG 5330 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : POST "/autobook/api/v1/journal/create", parameters={}
2019-05-09 1108.190 DEBUG 5330 --- [nio-8080-exec-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to public org.springframework.http.ResponseEntity<?> ....autobook.api.v1.controller.JournalController.create(java.security.Principal,e....autobook.db.model.VehicleJournal) throws com.fasterxml.jackson.core.JsonProcessingException
2019-05-09 1108.648 DEBUG 5330 --- [nio-8080-exec-3] m.m.a.RequestResponseBodyMethodProcessor : Read "application/json;charset=UTF-8" to [....autobook.db.model.VehicleJournal@764838f2]
Hibernate: insert into vehicle_journal (created, description, entry_time, milage, place, total_cost, updated, vehicle) values (?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into journal_entry (cost, fuel_amount_added, fuel_type, full_filled, name, unit_price, vehicle_journal) values (?, ?, ?, ?, ?, ?, ?)
2019-05-09 1109.145 WARN 5330 --- [nio-8080-exec-3] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1048, SQLState: 23000
2019-05-09 1109.147 ERROR 5330 --- [nio-8080-exec-3] o.h.engine.jdbc.spi.SqlExceptionHelper : (conn=112) Column 'vehicle_journal' cannot be null
2019-05-09 1109.196 DEBUG 5330 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : Failed to complete request: org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
2019-05-09 1109.241 ERROR 5330 --- [nio-8080-exec-3] o.s.b.w.servlet.support.ErrorPageFilter : Forwarding to error page from request [/api/v1/journal/create] due to exception [could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement]

org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:296) ~[spring-orm-5.1.5.RELEASE.jar:5.1.5.RELEASE]
...
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
at org.hibernate.exception.internal.SQLExceptionTypeDelegate.convert(SQLExceptionTypeDelegate.java:59) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
...
... 113 common frames omitted
Caused by: java.sql.SQLIntegrityConstraintViolationException: (conn=112) Column 'vehicle_journal' cannot be null
at org.mariadb.jdbc.internal.util.exceptions.ExceptionMapper.get(ExceptionMapper.java:229) ~[mariadb-java-client-2.3.0.jar:na]
...
... 179 common frames omitted
Caused by: java.sql.SQLException: Column 'vehicle_journal' cannot be null
Query is: insert into journal_entry (cost, fuel_amount_added, fuel_type, full_filled, name, unit_price, vehicle_journal) values (?, ?, ?, ?, ?, ?, ?), parameters [7000,40.35,5,0,'Orlen',218,<null>]
java thread: http-nio-8080-exec-3
...
... 184 common frames omitted

Zapomniałem dodać że przy pobieraniu rodzica VehicleJournal miałem błąd Infinite recursion dlatego do gettera w JournalEntry dodałem @JsonIgnore. Nie wiem czy to może mieć jakiś wpływ.

        @JsonIgnore
	public VehicleJournal getVehicleJournal() {
		return vehicleJournal;
	}

	public void setVehicleJournal(VehicleJournal vehicleJournal) {
		this.vehicleJournal = vehicleJournal;
	}

A więc znalazłem przyczynę, jednak to stanowiło problem. Usunąłem adnotację @JsonIgnore a dodałem:

public class JournalEntry {

//...
	
	**@JsonBackReference**
	@ManyToOne(fetch=FetchType.EAGER, optional=false, cascade= {CascadeType.ALL, CascadeType.PERSIST})
	@JoinColumn(name="vehicle_journal", referencedColumnName="id", nullable=false)
	private VehicleJournal vehicleJournal;

//...

public class VehicleJournal {
	
//...
	
	**@JsonManagedReference**
	@OneToMany(mappedBy="vehicleJournal", cascade= { CascadeType.ALL, CascadeType.PERSIST}, fetch=FetchType.LAZY)
	private Set<JournalEntry> journalEntries = new HashSet<>(0);

Wyniknął z tego inny problem, a mianowicie journalEntry zapisuje się bez klucza do fuelType.

1

Był problem bo @JsonIgnore powoduje że pole w ogóle nie jest mapowane z jsona na obiekt. Spróbuj dodać @Transactional do metody kontrolera.

1 użytkowników online, w tym zalogowanych: 0, gości: 1