Непоследовательное поведение каскадного сохранения по достижимости с JDO, App Engine, Data Nucleus, JUnit

Я экспериментирую с App Engine, используя JDO и DataNucleus для постоянства. У меня есть простой домен, который включает в себя несколько однонаправленных отношений. Возникает вопрос о вложении этих отношений:

  • Цивилизация -(1-1)->Клан
  • Цивилизация -(1-1)-> Земля
  • Цивилизация -(1-1)-> Военные -(1-N)-> Армии (это несовместимо)
  • Цивилизация -(1-N)-> Поселение

Согласно документации DataNucleus, семантика сохранения по достижимости должна сохраняйте все, каскадируя на сохранение Цивилизации. У меня есть тест JUnit для проверки базового хранения и извлечения этих объектов, но его поведение непоследовательно. Без изменений в коде повторные прогоны теста дают недетерминированные результаты. В частности, армии сохраняются только около 50% времени. Это единственный тест, который не проходит.

Мне было бы легче понять сценарий, в котором Армии никогда не сохраняются, но нерегулярное поведение ставит меня в тупик. Все остальное сохраняется корректно и стабильно. Я пытался обернуть фабричный метод в транзакцию, и я пробовал двунаправленные отношения, и ни один из них не изменил разделение 50/50 пройдено/не пройдено в JUnit.

Я использую конфигурацию на основе аннотаций для DataNucleus, как описано в документации App Engine (ссылка не включена из-за мер по борьбе со спамом). Приношу извинения за большое количество приложенного кода; Я просто не знаю, где я ошибаюсь.

ЦивилизацияCreateTest.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("[email protected]")
                                                .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> allSettlement = (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;
  }

}

Цивилизация.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;
  }
}

Военные.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);
  }

}

Армия.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;
  }
}

person Eric Grunzke    schedule 31.01.2011    source источник


Ответы (1)


Я предполагаю, что проблема заключается в том, как вы устанавливаете методы установки, которые принимают List. Помните, что JDO заменит поля ArrayList версиями, поддерживающими постоянство, поэтому вам не нужно изменять поля. Попробуй это:

@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);
  }

Это хорошая идея и по другим причинам. Вы не хотите, чтобы кто-то делал это:

military.setArmies(armies);
armies.clear();

...или это:

military.getArmies().clear();

Лично я хотел бы, чтобы методы, которые изменяют ваши сущности, выставляли только те операции, которые вы хотите:

public void addArmy(Army army) {
  armies.add(army);
}

public List<Army> getArmies() {
  return Collections.unmodifiableList(armies);
}
person NamshubWriter    schedule 05.02.2011
comment
Твои очки безопасности были очень полезны. В моей песочнице я действительно не учитывал это. Однако после переключения на стратегию clear() -> addAll() мой тест начал постоянно давать сбои, что я фактически считал прогрессом. Затем я добавил Army.save() в фабричный метод, и теперь он проходит последовательно. Мое лучшее предположение состоит в том, что косвенность означает, что армия больше недоступна, поэтому я должен сохранять ее сам. Меня все еще смущает недетерминированное поведение, но это работает и является лучшей практикой. - person Eric Grunzke; 06.02.2011
comment
Это озадачивает. Вы изменили все свои методы, которые принимают список, чтобы скопировать содержимое списка в список объекта? Интересно, получится ли у вас другое поведение, если ваш тест создаст менеджер постоянства после вызова CivilizationFactory.newInstance(). См. также stackoverflow.com/ вопросы/4185382/ - person NamshubWriter; 06.02.2011
comment
Основываясь на этой странице, вы хотите вызвать makePersistent в транзакции, если у объекта есть дочерние элементы: code.google.com/appengine/docs/java/datastore/jdo/ (я знаю, что вы пытались это сделать, но, возможно, когда вы это делали, вы все еще изменяли поля коллекции в своих объектах) - person NamshubWriter; 06.02.2011