JPA / Hibernate executa Insert seguido de Update para um mesmo registro novo

Estou tendo um problema com JPA / Hibernate onde ele está executando um INSERT seguido de um UPDATE para um novo registro que ele persiste.

Quero eliminar a execução do UPDATE, pois não faz sentido nenhum ele executar este segundo comando, pois não estou explicitamente fazendo isso. Alguma ideia do que pode causar essa redundância?

O modelo de entidade é assim (sem entrar no mérito do design):

PriceSummary [1] --> [N] ServicePriceSummary [1] --> [N] ServicePriceBreakdown [1] --> [N] MonthBreakdown

Devido a uma lógica de negócio particular, pode chegar a 10.000 ou 20.000 (sim 20 mil) registros de MonthBreakdown que são criados nos cálculos para uma grande proposta.

Fazendo um profiling da nossa aplicação, este é um grande gargalo, onde as operações do MonthBreakdown de INSERT e UPDATE, juntas, representam mais de 90% do processamento.

Se conseguir eliminar o UPDATE em já reduz em quase 50% o tempo de processamento.

Meu código (resumido) é, basicamente, o seguinte:

@Service
@Transactional
public class PricingService {

    /** Método chamado para realizar a transação. */
    public PricingContext calculate(Integer id) {
        Proposal proposal = proposalRepository.findOne(id);
        
		PriceSummary summary = new PriceSummary(proposal);
		PricingContext context = new PricingContext(summary);
		
		List<Service> services = serviceRepository.findByProposalId(id);
		services.forEach(service -> calculate(service, context));
		
		priceSummaryRepository.saveAndFlush(summary); // JpaRepository interface
		
		return context;
    }
	
	public void calculate(Service service, PricingContext context) {
	    List<Rates> rates = ratesService.findRatesForProposal(context.getProposal()); // do some JPA queries and return data
		
		// faz uns calculos muito loucos aqui e atualiza PricingSummary em context
	}
}

Entidade PriceSummary:

@Entity
@Table(name="price_summary")
public class PriceSummary {

    @Id @Column(name = "id")
    private Integer id;

    @Column(name = "lock_version")
    private Integer lockVersion;

    @Column(name = "price")
    private BigDecimal price;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "priceSummary", orphanRemoval = true)
    private final List<ServicePriceSummary> servicePriceSummaries = new ArrayList<>();
	
	// getters & setters
}

Entidade ServicePriceSummary:

@Entity
@Table(name="srv_price_summary")
public class ServicePriceSummary {

    @Id @Column(name = "id")
    private Integer id;

    @ManyToOne @JoinColumn(name = "price_summary_id")
    private PriceSummary priceSummary;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "servicePriceSummary", orphanRemoval = true)
	private final List<ServicePriceBreakdown> servicePriceBreakdowns = new ArrayList<>();
	
	// getters & setters
}

Entidade ServicePriceBreakdown:

@Entity
@Table(name="srv_price_breakdown")
public class ServicePriceBreakdown {

    @Id @Column(name = "id")
    private Integer id;

    @ManyToOne @JoinColumn(name = "srv_price_summary_id")
    private ServicePriceSummary servicePriceSummary;
	
	@Column(name = "country_code")
	private String countryCode;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "servicePriceBreakdown", orphanRemoval = true)
	private final List<MonthBreakdown> monthBreakdowns = new ArrayList<>();
	
	// getters & setters
}

Entidade MonthBreakdown:

@Entity
@Table(name="month_breakdown")
public class MonthBreakdown {

    @Id @Column(name = "id")
    private Integer id;

    @ManyToOne @JoinColumn(name = "srv_price_breakdown_id")
    private ServicePriceBreakdown servicePriceBreakdown;
	
	@Column(name = "month_number")
	private Integer month;
	
	@Column(name = "cost")
	private BigDecimal cost;
	
	// getters & setters
}

Curiosamente, se olhar o relatório do profiling do JProfiler, parece que ele executa o INSERT e depois o UPDATE quando se adiciona o registro em uma lista de um entidade. Porém no log SQL do Hibernate ele primeiro lista todos os INSERT e em seguida todos os UPDATE.

Outra estratégia foi configurar o Batch Size do Hibernate. Com isso tivemos algum ganho (20 a 40% em alguns casos), mas ainda não resolveu o problema.

Uma outra estratégia que pensamos foi remover a configuração de “cascade = CascadeType.ALL” da classe ServicePriceBreakdown no atributo monthBreakdowns e não deixar o Hibernate executar os INSERTS para o MonthBreakdown. Então, após salvar os objetos do grafo, reuno uma lista dos MonthBreakdown e explicitamente salvo, visando evita o UPDATE e melhorar execução em batch. Faz sentido?

Estamos usando OpenJDK 1.8, Spring Boot 1.3.1, Tomcat 7.

Destro, veja se isso te ajuda:

Teste isso na ServicePriceBreakdown alterando o mapeamento com a MonthBreakdown

Abraços.

O código está configurado assim, com mapeamento do relacionamento bi-direcional. Vou criar um projeto mínimo para tentar reproduzir o problema e compartilhar no github. tks

Destro, o problema não é na bidirecionalidade, é no tipo de mapeamento.

Está assim:

public class ServicePriceBreakdown {
   	@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "servicePriceBreakdown", orphanRemoval = true)
	private final List<MonthBreakdown> monthBreakdowns = new ArrayList<>();
}

public class MonthBreakdown {
    @ManyToOne @JoinColumn(name = "srv_price_breakdown_id")
    private ServicePriceBreakdown servicePriceBreakdown;
}

Para evitar o update tem que ser assim:

public class ServicePriceBreakdown {
   	@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name="srv_price_breakdown_id")
	private final List<MonthBreakdown> monthBreakdowns = new ArrayList<>();
}

public class MonthBreakdown {
    @ManyToOne @JoinColumn(name = "srv_price_breakdown_id")
    private ServicePriceBreakdown servicePriceBreakdown;
}

Você precisa repetir a anotação @JoinColumn nos dois lados. A questão é que no mapeamento xml havia um atributo “inverse=true” que não existe na anotação, porém quando você utiliza o JoinColumn no mapeamento OneToMany, você explicitamente diz “inverse=true” e aí ele para de fazer update depois de insert.

Outro ponto: se você precisa manipular milhares de registros, não seria melhor adotar alguma base nosql?

Boa sorte.

Se introduzo a anotacao eu ganho uma exception ao executar.

Caused by: org.hibernate.AnnotationException: Associations marked as mappedBy must not define database mappings like @JoinTable or @JoinColumn: my.app.entity.ServicePriceBreakdown.monthBreakdowns

Mas aí você tira o mappedBy, é ou um ou outro.

Mas o mappedBy substitui o antigo inverse=true. Vou realizar mais uns testes e atualizo aqui.