Inconsistent cascading persistence-by-reachability behavior with JDO, App Engine, Data Nucleus, JUnit
I'm experimenting with App Engine, using JDO and DataNucleus for persistence. I have a simple domain that includes several unidirectional relationships. The question comes with nesting those relationships:
- Civilization -(1-1)-> Clan
- Civilization -(1-1)-> Land
- Civilization -(1-1)-> Military -(1-N)-> Armies (this is inconsistent)
- Civilization -(1-N)-> Settlement
According to the DataNucleus Documentation, persistence-by-reachability semantics should persist everything by cascading on a persist of a Civilization. I have a JUnit test to check the basic storage and retrieval of these objects, but its behavior is inconsistent. With no changes to the code, repeated runs of the test give nondeterministic results. Specifically, the armies only persist about 50% of the time. They are the only test that fails.
I could more easily understand a scenario where Armies never persist, but the irregular behavior has me at a loss. Everything else persists correctly and consistently. I've tried wrapping the factory method in a transaction and I've tried bidirectional relationships, and neither have changed the 50/50 pass/fail split in JUnit.
I am using Annotation-based configuration for DataNucleus, as described in the App Engine documentation (link not included because of anti-spam measures). I apologize for the large amount of code attached; I just don't know where I'm going wrong.
CivilizationCreateTest.java:
package com.moffett.grunzke.server;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.moffett.grunzke.generic.GenericHelperFactory;
import com.moffett.grunzke.server.civilization.Army;
import com.moffett.grunzke.server.civilization.Civilization;
import com.moffett.grunzke.server.civilization.Clan;
import com.moffett.grunzke.server.civilization.Land;
import com.moffett.grunzke.server.civilization.Military;
import com.moffett.grunzke.server.civilization.Settlement;
@SuppressWarnings("unchecked")
public class CivilizationCreationTest
{
private final LocalServiceTestHelper helper = new LocalServiceTestHelper(
new LocalDatastoreServiceTestConfig(),
new LocalUserServiceTestConfig())
.setEnvIsLoggedIn(true)
.setEnvEmail("generic.user@gmail.com")
.setEnvAuthDomain("google.com");
@Before
public void setUp()
{
helper.setUp();
}
@After
public void tearDown()
{
helper.tearDown();
}
@Test
public void testCivilizationCreation()
{
String clanName = "Test Clan";
String rulerName = "Test Ruler";
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
if (user == null)
{
fail("No user");
}
PersistenceManager pm = PMF.get().getPersistenceManager();
CivilizationFactory.newInstance(user, clanName, rulerName);
// We check to make sure that 1, and only 1 Civilization has been made.
Query q1 = pm.newQuery("SELECT FROM " + Civilization.class.getName());
List<Civilization> allCivilizations = (List<Civilization>) q1.execute();
assertTrue(allCivilizations.size() == 1);
// Now we move on to checking the other aspects.
Civilization persistentCiv = allCivilizations.get(0);
Clan persistentClan = persistentCiv.getClan();
Land persistentLand = persistentCiv.getLand();
Military persistentMilitary = persistentCiv.getMilitary();
ArrayList<Settlement> persistentSettlements = persistentCiv.getSettlements();
// Make sure Civ has pointers to all the necessary elements.
assertTrue(persistentClan != null);
assertTrue(persistentLand != null);
assertTrue(persistentMilitary != null);
assertTrue(persistentMilitary.getArmies() != null);
assertTrue(persistentSettlements != null);
// Lastly we want to make sure that there is only one entry in each of Clan,
// Land, Military, Army, Settlement.
Query q2 = pm.newQuery("SELECT FROM " + Clan.class.getName());
List<Clan> allClans = (List<Clan>) q2.execute();
assertTrue(allClans.size() == 1);
Query q3 = pm.newQuery("SELECT FROM " + Land.class.getName());
List<Land> allLand = (List<Land>) q3.execute();
assertTrue(allLand.size() == 1);
Query q4 = pm.newQuery("SELECT FROM " + Military.class.getName());
List<Military> allMilitary = (List<Military>) q4.execute();
assertTrue(allMilitary.size() == 1);
Query q5 = pm.newQuery("SELECT FROM " + Army.class.getName());
List<Army> allArmy = (List<Army>) q5.execute();
// *** THIS FAILS 50% OF THE TIME ***
assertTrue(allArmy.size() == 1);
Query q6 = pm.newQuery("SELECT FROM " + Settlement.class.getName());
List<Settlement> allSettl开发者_运维知识库ement = (List<Settlement>) q6.execute();
assertTrue(allSettlement.size() == 1);
}
}
CivilizationFactory.java:
package com.moffett.grunzke.server;
import java.util.ArrayList;
import com.google.appengine.api.users.User;
import com.moffett.grunzke.server.civilization.Army;
import com.moffett.grunzke.server.civilization.Civilization;
import com.moffett.grunzke.server.civilization.Clan;
import com.moffett.grunzke.server.civilization.Land;
import com.moffett.grunzke.server.civilization.Military;
import com.moffett.grunzke.server.civilization.Settlement;
public class CivilizationFactory
{
public static Civilization newInstance(User user, String clanName, String rulerName)
{
// First we make a new clan.
Clan clan = new Clan();
clan.setUser(user);
clan.setClanName(clanName);
clan.setRulerName(rulerName);
// Don't need land.save() because of persistence-by-reachability
// Now we need to make a new Land.
Land land = new Land();
land.setArableLand(100);
land.setPasturableLand(0);
land.setLandUsedBySettlements(0);
// Don't need land.save() because of persistence-by-reachability
// Now we need to make a new Military
Military military = new Military();
Army army = new Army();
army.setMeleeUnits(10);
army.setRangedUnits(10);
army.setMountedUnits(10);
military.addArmy(army);
// Don't need military.save() because of persistence-by-reachability
// Now we need to make a new Settlement
Settlement settlement = new Settlement();
// Don't need settlement.save() because of persistence-by-reachability
ArrayList<Settlement> settlements = new ArrayList<Settlement>();
settlements.add(settlement);
// Lastly join everything together in the civ
Civilization civ = new Civilization();
civ.setClan(clan);
civ.setLand(land);
civ.setMilitary(military);
civ.setSettlements(settlements);
civ.save();
// civ.save should casacde to cover all of the elements above
return civ;
}
}
Civilization.java:
package com.moffett.grunzke.server.civilization;
import java.util.ArrayList;
import javax.jdo.PersistenceManager;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.datastore.Key;
import com.moffett.grunzke.server.PMF;
@PersistenceCapable
public class Civilization
{
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Key key;
@Persistent
private Clan clan;
@Persistent
private Land land;
@Persistent
private Military military;
@Persistent
private ArrayList<Settlement> settlements = new ArrayList<Settlement>();
public void save()
{
PersistenceManager pm = PMF.get().getPersistenceManager();
try
{
pm.makePersistent(this);
}
finally
{
pm.close();
}
}
public ArrayList<Settlement> getSettlements()
{
return settlements;
}
public void setSettlements(ArrayList<Settlement> settlements)
{
this.settlements = settlements;
}
public Key getKey()
{
return key;
}
public void setKey(Key key)
{
this.key = key;
}
public Clan getClan()
{
return clan;
}
public void setClan(Clan clan)
{
this.clan = clan;
}
public Land getLand()
{
return land;
}
public void setLand(Land land)
{
this.land = land;
}
public void setMilitary(Military military)
{
this.military = military;
}
public Military getMilitary()
{
return military;
}
}
Military.java
package com.moffett.grunzke.server.civilization;
import java.util.ArrayList;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.datastore.Key;
@PersistenceCapable
public class Military
{
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Key key;
@Persistent
private ArrayList<Army> armies = new ArrayList<Army>();
public Key getKey()
{
return key;
}
public void setKey(Key key)
{
this.key = key;
}
public ArrayList<Army> getArmies()
{
return armies;
}
public void setArmies(ArrayList<Army> armies)
{
this.armies = armies;
}
public void addArmy(Army army)
{
this.armies.add(army);
}
}
Army.java
package com.moffett.grunzke.server.civilization;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.datastore.Key;
@PersistenceCapable
public class Army
{
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Key key;
@Persistent
private int meleeUnits;
@Persistent
private int rangedUnits;
@Persistent
private int mountedUnits;
public Key getKey()
{
return key;
}
public void setKey(Key key)
{
this.key = key;
}
public int getMeleeUnits()
{
return meleeUnits;
}
public void setMeleeUnits(int meleeUnits)
{
this.meleeUnits = meleeUnits;
}
public int getRangedUnits()
{
return rangedUnits;
}
public void setRangedUnits(int rangeUnits)
{
this.rangedUnits = rangeUnits;
}
public int getMountedUnits()
{
return mountedUnits;
}
public void setMountedUnits(int mountedUnits)
{
this.mountedUnits = mountedUnits;
}
}
My guess is the problem is with the way you are setting the setter methods that take a List
are implemented. Remember that JDO will replace the ArrayList fields with persistance-aware versions, so you don't want to change the fields. Try this:
@PersistenceCapable
public class Military {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Key key;
@Persistent
private final List <Army> armies = new ArrayList<Army>();
public void setArmies(List<Army> armies) {
this.armies.clear();
this.armies.addAll(armies);
}
This is a good idea for other reasons as well. You don't want someone doing this:
military.setArmies(armies);
armies.clear();
...or this:
military.getArmies().clear();
Personally, I would have the methods that change your entities expose only the operations you want:
public void addArmy(Army army) {
armies.add(army);
}
public List<Army> getArmies() {
return Collections.unmodifiableList(armies);
}
精彩评论