Spring Data JPA Однонаправленное сопоставление OneToOne не сохраняется - PullRequest
0 голосов
/ 16 декабря 2018

У меня есть тип клиента (представляющий клиента), у которого есть принципал типа Person и контакт типа Contact (для контактных данных).

Тип данных Client (с сопоставлениями):

@Entity
public class Client {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Access(AccessType.PROPERTY)
    private Long id;

    @Column(unique = true, updatable = false)
    private String nk;

    @Min(10000000000L)
    private Long abn;

    private String name;

    @OneToOne(cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    @JoinColumn(name = "person_id")
    private Person principal;

    @OneToOne(cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    @JoinColumn(name = "contact_id")
    private Contact contact;

    public Client() {
        this.nk = UUID.randomUUID().toString();
    }

    // remaining constructors, builders and accessors omitted for brevity
}

Использование контакта в качестве примера (Person структурирован аналогично)

@Entity
public class Contact {
    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    private String phone;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String mobile;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String fax;

    @OneToOne(cascade={CascadeType.PERSIST,CascadeType.REMOVE})
    @JoinColumn(name = "address_id")
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private Address address;

    private String email;

    public Contact() {
    }
    // remaining constructors, builders and accessors omitted for brevity
}

Тип данных Address также является однонаправленным отображением.Я мог бы показать Персона и Адрес, но я почти уверен, что из них ничего нового не поймут.

К сожалению, Клиент дважды становится объектом отношений ManyToOne с самолетами через поля владельца и оператора.Я говорю, к сожалению, потому что это усложняет мой вопрос.Тип данных Aircraft выглядит следующим образом:

@Entity
public class Aircraft {

    @Id
    private String registration;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "casa_code")
    private Casa casa;

    private String manufacturer;

    private String model;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-d")
    private LocalDate manufacture;

    @ManyToOne(cascade={CascadeType.PERSIST}, fetch = FetchType.EAGER)
    private Client owner;

    @ManyToOne(cascade={CascadeType.PERSIST}, fetch = FetchType.EAGER)
    private Client operator;

    private String base;

    @OneToOne(cascade={CascadeType.ALL})
    @JoinColumn(name = "airframe_id")
    private Airframe airframe;

    @OneToMany(cascade={CascadeType.ALL}, fetch = FetchType.EAGER)
    @JoinColumn(name = "ac_registration")
    private Set<Prop> props;

    @OneToMany(cascade={CascadeType.ALL}, fetch = FetchType.EAGER)
    @JoinColumn(name = "ac_registration")
    private Set<Engine> engines;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-d")
    private LocalDate lastUpdated;

    public Aircraft() {
    }
    // remaining constructors, builders and accessors omitted for brevity

}

Метод контроллера, ответственный за сохранение:

@RestController
@RequestMapping("/api")
public class AircraftController {
    private static final Logger LOG = LoggerFactory.getLogger(AircraftController.class);

    private AircraftService aircraftService;

    @Autowired
    public AircraftController(AircraftService aircraftService) {
        this.aircraftService = aircraftService;
    }

    @Secured("ROLE_ADMIN")
    @PostMapping(value="/maintainers/{mid}/aircrafts", produces = "application/json")
    @ResponseStatus(value = HttpStatus.CREATED)
    Response<Aircraft> create(@PathVariable("mid") Long mid, @RequestBody Aircraft request) {

        return Response.of(aircraftService.create(request));
    }
}

Метод обслуживания:

public interface AircraftService {

    Aircraft create(Aircraft aircraft);

    // other interface methods omitted for brevity

    @Service
    class Default implements AircraftService {

        private static final Logger LOG = LoggerFactory.getLogger(AircraftService.class);

        @Autowired
        AircraftRepository aircraftRepository;
        @Transactional
        public Aircraft create(Aircraft aircraft) {
            LOG.debug("creating new Aircraft with {}", aircraft);
            if (aircraft.getOwner() != null && aircraft.getOwner().getNk() == null) {
                aircraft.getOwner().setNk(UUID.randomUUID().toString());
            }
            if (aircraft.getOperator() != null && aircraft.getOperator().getNk() == null) {
                aircraft.getOperator().setNk(UUID.randomUUID().toString());
            }
            return aircraftRepository.save(aircraft);
        }
    }
}

и, наконец,хранилище:

@Repository
public interface AircraftRepository extends JpaRepository<Aircraft, String> {
}

Когда я поставляю следующий JSON:

{
  "registration":"VH-ZZZ",
  "casa":null,
  "manufacturer":"PITTS AVIATION ENTERPRISES",
  "model":"S-2B",
  "manufacture":"1983-01-2",
  "owner":{
    "id":null,
    "nk":"84f5f053-82dd-4563-8158-e804b3003f3b",
    "abn":null,
    "name":"xxxxx, xxxxx xxxxxx",
    "principal":null,
    "contact":{
      "id":null,
      "phone":null,
      "address":{
        "id":null,
        "line3":"PO Box xxx",
        "suburb":"KARAMA",
        "postcode":"0813",
        "state":"NT",
        "country":"Australia"
      },
      "email":null
    }
  },
  "operator":{
    "id":null,
    "nk":"edfd3e41-664c-4832-acc5-1c04d9c673a3",
    "abn":null,
    "name":"xxxxx, xxxxx xxxxxx",
    "principal":null,
    "contact":{
      "id":null,
      "phone":null,
      "address":{
        "id":null,
        "line3":"PO Box xxx",
        "suburb":"KARAMA",
        "postcode":"0813",
        "state":"NT",
        "country":"Australia"
      },
      "email":null
    }
  },
  "base":null,
  "airframe":{
    "id":null,
    "acRegistration":null,
    "serialNumber":"5005",
    "hours":null
  },
  "props":[
    {
      "id":null,
      "acRegistration":"VH-ZZZ",
      "engineNumber":1,
      "make":"HARTZELL PROPELLERS",
      "model":"HC-C2YR-4CF/FC8477A-4",
      "casa":null,
      "serialNumber":null,
      "hours":null
    }
  ],
  "engines":[
    {
      "id":null,
      "acRegistration":"VH-ZZZ",
      "engineNumber":1,
      "make":"TEXTRON LYCOMING",
      "model":"AEIO-540",
      "casa":null,
      "serialNumber":null,
      "hours":null
    }
  ],
  "lastUpdated":"2018-12-16"
}

К этому тесту:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("embedded")
@EnableJpaRepositories({ "au.com.avmaint.api" })
@AutoConfigureMockMvc
public class AircraftControllerFunctionalTest {

    private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
            MediaType.APPLICATION_JSON.getSubtype(),
            Charset.forName("utf8"));

    private HttpMessageConverter mappingJackson2HttpMessageConverter;

    @TestConfiguration
    static class ServiceImplTestContextConfiguration {

        @Bean
        public CasaFixtures casaFixtures() {
            return new CasaFixtures.Default();
        }

        @Bean
        public ModelFixtures modelFixtures() {
            return new ModelFixtures.Default();
        }

        @Bean
        public MaintainerFixtures maintainerFixtures() {
            return new MaintainerFixtures.Default();
        }

        @Bean
        public AircraftFixtures aircraftFixtures() {
            return new AircraftFixtures.Default();
        }
    }

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private MockMvc mvc;

    @Autowired
    private UserService userService;

    @Autowired
    private RoleService roleService;

    @Autowired
    private ModelFixtures modelFixtures;

    @Autowired
    private MaintainerFixtures maintainerFixtures;

    @Autowired
    private AircraftFixtures aircraftFixtures;

    Maintainer franks;

    @Autowired
    void setConverters(HttpMessageConverter<?>[] converters) {

        this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream()
                .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter)
                .findAny()
                .orElse(null);

        assertNotNull("the JSON message converter must not be null",
                this.mappingJackson2HttpMessageConverter);
    }


    @Before
    public void setup() {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                .build();

        franks = maintainerFixtures.createFranksMaintainer();

    }

    @After
    public void tearDown() {

        maintainerFixtures.removeFranks(franks);
        aircraftFixtures.killAircraft(aircrafts);
        UserAndRoleFixtures.killAllUsers(userService, roleService);
    }

    @Test
    public void doCreate() throws Exception {

        File file = ResourceUtils.getFile("classpath:json/request/vh-zzz.json");
        String json = new String(Files.readAllBytes(file.toPath()));

        mvc.perform((post("/api/maintainers/{mid}/aircrafts", franks.getId())
                .header(AUTHORIZATION_HEADER, "Bearer " + ModelFixtures.ROOT_JWT_TOKEN))
                .content(json)
                .contentType(contentType))
                .andDo(print())
                .andExpect(status().isCreated())
                .andExpect(content().contentType(contentType))
                .andExpect(jsonPath("$.payload").isNotEmpty())
                .andExpect(jsonPath("$.payload.registration").value("VH-ZZZ"))
                .andExpect(jsonPath("$.payload.manufacturer").value("PITTS AVIATION ENTERPRISES"))
                .andExpect(jsonPath("$.payload.model").value("S-2B"))
                .andExpect(jsonPath("$.payload.manufacture").value("1983-01-2"))
                .andExpect(jsonPath("$.payload.owner.id").isNotEmpty())
                .andExpect(jsonPath("$.payload.owner.nk").isNotEmpty())
                .andExpect(jsonPath("$.payload.owner.contact").isNotEmpty())
                .andExpect(jsonPath("$.payload.operator.id").isNotEmpty())
                .andExpect(jsonPath("$.payload.operator.nk").isNotEmpty())
                .andExpect(jsonPath("$.payload.operator.contact").isNotEmpty())
        ;
    }

}

Я обнаружил, что два отображения ManyToOne вСамолеты сохраняются, а сопоставления OneToOne - нет.В основном эти два ожидания не оправдываются:

.andExpect(jsonPath("$.payload.owner.contact").isNotEmpty())
.andExpect(jsonPath("$.payload.operator.contact").isNotEmpty())

Я пробовал несколько вариантов каскадирования, таких как ALL, MERGE и т. Д., И, похоже, мой пример во многом похож на другие.Я понимаю, что это немного необычно в том, что Адрес, Персона и Контакт не содержат ссылок на их родителей, но я бы подумал, что это точка однонаправленных отношений.Кто-нибудь знает, как их сохранить?

ОБНОВЛЕНИЕ - В методе создания в AircraftService я попытался сохранить владельца и операторов отдельно с помощью:

@Transactional
public Aircraft create(Aircraft aircraft) {
    if (aircraft.getOwner() != null && aircraft.getOwner().getNk() == null) {
        aircraft.getOwner().setNk(UUID.randomUUID().toString());
    } else if (aircraft.getOwner() != null) {
        if (aircraft.getOwner().getPrincipal() != null) {
            LOG.debug("saving principal");
            Person person = personRepository.save(aircraft.getOwner().getPrincipal());
            aircraft.getOwner().setPrincipal(person);
        }
        if (aircraft.getOwner().getContact() != null) {
            Contact contact = contactRepository.save(aircraft.getOwner().getContact());
            aircraft.getOwner().setContact(contact);
        }
    }
    if (aircraft.getOperator() != null && aircraft.getOperator().getNk() == null) {
        aircraft.getOperator().setNk(UUID.randomUUID().toString());
        if (aircraft.getOperator().getPrincipal() != null) {
            Person person = personRepository.save(aircraft.getOperator().getPrincipal());
            aircraft.getOperator().setPrincipal(person);
        }
        if (aircraft.getOperator().getContact() != null) {
            Contact contact = contactRepository.save(aircraft.getOperator().getContact());
            aircraft.getOperator().setContact(contact);
        }
    }

    return aircraftRepository.save(aircraft);
}

Я сделал это, предполагая, что JPA не знает, что делать со ссылками на объекты, которые еще не существуют.

Но без изменений.

Кроме сохранения идентификаторов контактов и лицэто изменение не имеет значения для репозитория сохранения, который все еще не может связать Клиента с Контактом или Лицом.Нужно ли сохранять контакт и лицо в отдельной транзакции, а затем сохранять клиента?

Это меня убивает: что происходит?

1 Ответ

0 голосов
/ 19 декабря 2018

Проблема заключалась в том, что мне нужно было CascadeType.MERGE для объекта "Самолет":

@ManyToOne(cascade={CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.EAGER)
private Client owner;

@ManyToOne(cascade={CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.EAGER)
private Client operator;

По сути, когда JSON вводится в операцию создания, он кажется неотличимым от операции MERGE (где новыйсущность создается и помещается под управление, в отличие от PERSIST), поэтому операция MERGE передается клиенту, а не PERSIST (как я и ожидал).Вот почему Персона и Контакт не были сохранены.Я до сих пор не понимаю, почему ввод JSON трактуется как слияние - это не имеет смысла.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...