Skip to content

Commit

Permalink
GH-818 - Flush mapping context on potentially custom write queries.
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-simons committed Aug 21, 2020
1 parent 374a349 commit ab7bbf8
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
* @author Vince Bickers
* @author Luanne Misquitta
* @author Mark Angrish
* @author Michael J. Simons
*/
public class MappingContext {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
*/
public class ExecuteQueriesDelegate extends SessionDelegate {

private static final Pattern WRITE_CYPHER_KEYWORDS = Pattern.compile("\\b(CREATE|MERGE|SET|DELETE|REMOVE|DROP)\\b");
private static final Pattern WRITE_CYPHER_KEYWORDS = Pattern.compile("\\b(CREATE|MERGE|SET|DELETE|REMOVE|DROP|CALL)\\b");
private static final Set<Class<?>> VOID_TYPES = new HashSet<>(Arrays.asList(Void.class, void.class));

public ExecuteQueriesDelegate(Neo4jSession session) {
Expand Down Expand Up @@ -142,8 +142,16 @@ public Result query(String cypher, Map<String, ?> parameters, boolean readOnly)

private <T> Iterable<T> executeAndMap(Class<T> type, String cypher, Map<String, ?> parameters) {

return session.<Iterable<T>>doInTransaction(() -> {
return session.doInTransaction(() -> {

// While an update query may not return objects, it has enough changes
// to modify all entities in the context, so we must flush it either way.
if (mayBeReadWrite(cypher)) {
session.clear();
}

if (type != null && session.metaData().classInfo(type.getName()) != null) {

// Things that can be mapped to entities
GraphModelRequest request = new DefaultGraphModelRequest(cypher, parameters);
try (Response<GraphModel> response = session.requestHandler().execute(request)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2002-2020 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.ogm.domain.gh817;

import org.neo4j.ogm.annotation.GeneratedValue;
import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.NodeEntity;

/**
* @author Michael J. Simons
*/
@NodeEntity
public class Bike {

@Id @GeneratedValue
private Long id;

private String name;

private boolean damaged;

public Bike() {
}

public Bike(String name) {
this.name = name;
this.damaged = false;
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public boolean isDamaged() {
return damaged;
}

public void setDamaged(boolean damaged) {
this.damaged = damaged;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2002-2020 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.ogm.domain.gh817;

import java.util.ArrayList;
import java.util.List;

import org.neo4j.ogm.annotation.GeneratedValue;
import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;

/**
* @author Michael J. Simons
*/
@NodeEntity
public class Rider {

@Id @GeneratedValue
private Long id;

private String name;

@Relationship(type = "RODE")
private List<Trip> trips = new ArrayList<>();

public Rider() {
}

public Rider(String name) {
this.name = name;
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<Trip> getTrips() {
return trips;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2002-2020 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.ogm.domain.gh817;

import org.neo4j.ogm.annotation.EndNode;
import org.neo4j.ogm.annotation.GeneratedValue;
import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.RelationshipEntity;
import org.neo4j.ogm.annotation.StartNode;

/**
* @author Michael J. Simons
*/
@RelationshipEntity("RODE")
public class Trip {

@Id @GeneratedValue
private Long id;

@StartNode
private Rider rider;

@EndNode
private Bike bikeUsed;

private String name;

private double length;

public Trip() {
}

public Trip(Rider rider, Bike bikeUsed, String name) {
this.rider = rider;
this.bikeUsed = bikeUsed;
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getLength() {
return length;
}

public void setLength(double length) {
this.length = length;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,33 @@

import java.io.IOException;
import java.util.Collections;
import java.util.stream.Stream;

import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.neo4j.ogm.context.MappingContext;
import org.neo4j.ogm.domain.cineasts.annotated.Actor;
import org.neo4j.ogm.domain.cineasts.annotated.Knows;
import org.neo4j.ogm.domain.gh817.Bike;
import org.neo4j.ogm.domain.gh817.Rider;
import org.neo4j.ogm.domain.gh817.Trip;
import org.neo4j.ogm.domain.music.Album;
import org.neo4j.ogm.domain.music.Artist;
import org.neo4j.ogm.domain.music.Recording;
import org.neo4j.ogm.domain.music.ReleaseFormat;
import org.neo4j.ogm.domain.music.Studio;
import org.neo4j.ogm.model.Result;
import org.neo4j.ogm.session.Neo4jSession;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.neo4j.ogm.testutil.TestContainersTestBase;
import org.neo4j.ogm.transaction.Transaction;

/**
* @author Mihai Raulea
* @see ISSUE-86
* @author Michael J. Simons
*/
public class SessionAndMappingContextTest extends TestContainersTestBase {

Expand All @@ -60,10 +66,17 @@ public class SessionAndMappingContextTest extends TestContainersTestBase {
private Knows knows;
private Knows knows2;

private static SessionFactory sessionFactory;

@BeforeClass
public static void oneTimeSetUp() {
sessionFactory = new SessionFactory(getDriver(), "org.neo4j.ogm.domain.music",
"org.neo4j.ogm.domain.cineasts.annotated", "org.neo4j.ogm.domain.gh817");
}

@Before
public void init() throws IOException {
session = (Neo4jSession) new SessionFactory(getDriver(), "org.neo4j.ogm.domain.music",
"org.neo4j.ogm.domain.cineasts.annotated").openSession();
session = (Neo4jSession) sessionFactory.openSession();

artist1 = new Artist();
artist1.setName("MainArtist");
Expand Down Expand Up @@ -222,31 +235,81 @@ public void shouldRollbackRelationshipEntityWithDifferentStartAndEndNodes() {
}

@Test
public void shouldWhat() {
public void shouldNotThrowConcurrentModificationException() {

Actor mary = new Actor("Mary");
try (Transaction tx = session.beginTransaction()) {
session.save(new Actor("Mary"));
session.deleteAll(Actor.class);
}
}

Knows maryKnowsMary = new Knows();
@Test // GH-817
public void shouldRefreshUpdatedEntities() {
sessionFactory.openSession().purgeDatabase();

maryKnowsMary.setFirstActor(mary);
maryKnowsMary.setSecondActor(mary);
Session sessionForCreation = sessionFactory.openSession();
try (Transaction tx = sessionForCreation.beginTransaction()) {
Stream.of(new Bike("Bike1"), new Bike("Bike2")).forEach(sessionForCreation::save);
tx.commit();
}

mary.getKnows().add(maryKnowsMary);
Session sessionForLoadingAndUpdate = sessionFactory.openSession();
Iterable<Bike> loadedBikes = sessionForLoadingAndUpdate.loadAll(Bike.class);
assertThat(loadedBikes).hasSize(2).extracting(Bike::isDamaged).containsOnly(false);

try (Transaction tx = session.beginTransaction()) {
Iterable<Bike> updatedBikes = sessionForLoadingAndUpdate
.query(Bike.class, "MATCH (c:Bike) SET c.damaged = true RETURN c", Collections.emptyMap());

assertThat(updatedBikes).hasSize(2).extracting(Bike::isDamaged).containsOnly(true);
}

session.save(mary);
@Test // GH-817
public void shouldFlushSessionWithoutReturningNodes() {
sessionFactory.openSession().purgeDatabase();

session.context().reset(mary);
Session sessionForCreation = sessionFactory.openSession();
try (Transaction tx = sessionForCreation.beginTransaction()) {
Stream.of(new Bike("Bike1"), new Bike("Bike2")).forEach(sessionForCreation::save);
tx.commit();
}
}

@Test
public void shouldNotThrowConcurrentModificationException() {
Session sessionForLoadingAndUpdate = sessionFactory.openSession();
Iterable<Bike> loadedBikes = sessionForLoadingAndUpdate.loadAll(Bike.class);
assertThat(loadedBikes).hasSize(2).extracting(Bike::isDamaged).containsOnly(false);

try (Transaction tx = session.beginTransaction()) {
session.save(new Actor("Mary"));
session.deleteAll(Actor.class);
sessionForLoadingAndUpdate
.query(void.class, "MATCH (c:Bike) SET c.damaged = true", Collections.emptyMap());

loadedBikes = sessionForLoadingAndUpdate.loadAll(Bike.class);
assertThat(loadedBikes).hasSize(2).extracting(Bike::isDamaged).containsOnly(true);
}

@Test // GH-817
public void shouldRefreshUpdatedRelationshipEntities() {
sessionFactory.openSession().purgeDatabase();

Session sessionForCreation = sessionFactory.openSession();
Rider rider = new Rider("Michael");
Bike bike1 = new Bike("Bike1");
Bike bike2 = new Bike("Bike2");
rider.getTrips().add(new Trip(rider, bike1, "n/a"));
rider.getTrips().add(new Trip(rider, bike2, "n/a"));
try (Transaction tx = sessionForCreation.beginTransaction()) {
sessionForCreation.save(rider);
tx.commit();
}

Session sessionForLoadingAndUpdate = sessionFactory.openSession();
Iterable<Trip> loadedBikes = sessionForLoadingAndUpdate.loadAll(Trip.class);
assertThat(loadedBikes).hasSize(2).extracting(Trip::getName).containsOnly("n/a");

String name = "A nice trip";
Iterable<Trip> updatedBikes = sessionForLoadingAndUpdate
.query(Trip.class, "MATCH (r:Rider) - [t:RODE] -> (c:Bike) SET t.name = $newName RETURN *", Collections.singletonMap("newName", name));

Rider loadedRider = sessionForLoadingAndUpdate.load(Rider.class, rider.getId());

assertThat(updatedBikes).hasSize(2).extracting(Trip::getName).containsOnly(name);
assertThat(loadedRider.getTrips()).hasSize(2).extracting(Trip::getName).containsOnly(name);
}
}

0 comments on commit ab7bbf8

Please sign in to comment.