質問

Executive summary:

When I get an object back from play framework's "Finder", that object's ManyToOne field is correctly populated, but the object that it points to itself has OneToMany fields that are erroneously null.

In my app (a "Scenario Repository"), a Scenario has a Game, and Games have many Scenarios.

When I "find" a Scenario

  Scenario retrieved_scenario = Scenario.find.where().eq("name", "Test Scenario").findUnique();

the Game object (retrieved_scenario.game) is missing it's scenarios:

    assertNotNull(retrieved_scenario);   // passes

    assertEquals("CM Test", retrieved_scenario.game.name); //passes

    assertNotNull("retrived scenario's game's scenarios", retrieved_scenario.game.scenarios);  // fails

But if I retreive the Game itself using find for the Game, and test its scenarios list, it is fine.

What am I doing wrong?

Details:

I have an ebean persisted object called a Game that can have many Scenarios:

import play.db.ebean.*;
import com.avaje.ebean.*;

@Entity
public class Game extends Model {

    @Id
    public String name;

    @OneToMany
    public List<Scenario> scenarios;

The Scenario object (also ebean persisted) adds itself to the Game's scenario list when it is created:

@Entity
public class Scenario extends Model {

    @Id
    public String name;

    @ManyToOne
    public Game game;

    @OneToOne
    public User author;

    public Scenario(String name, String game_name, String author_name) {
        Logger.info(String.format("Constructing scenario '%s' of type '%s'", name, game_name));
        this.name = name;
        this.game = Game.find.where().eq("name", game_name).findUnique();
        Logger.info(String.format("...found it's game: '%s', which has %d scenarios", this.game.name, this.game.scenarios.size()));

        this.game.scenarios.add(this);
        Logger.info(String.format("now ... it's game has %d scenarios", this.game.scenarios.size()));
        this.author = User.find.where().eq("username", author_name).findUnique();
    }

    public static Scenario create(String name, String game_name, String author_name) {
        Logger.info(String.format("Creating scenario '%s' of '%s' by '%s'", name, game_name, author_name));
        Scenario it = new Scenario(name, game_name, author_name);
        it.save();
        it.game.save();
        it.game.saveManyToManyAssociations("scenarios");  // doesn't help (or apparently hurt)
        return it;
    }

    public static Scenario.Finder<String,Scenario> find = 
    new Scenario.Finder<String,Scenario>(String.class, Scenario.class); 

}

However, when I "find" a scenario using this finder, the scenarios of the associated game are not initialised:

@Test
public void createAndRetrieveScenario() {
    Logger.info("testing createAndRetrieveScenario...");

    Logger.info("setting up...");
    Game created_game = Game.create("CM Test");

    Logger.info("creating scenario...");
    Scenario created_scenario = Scenario.create("Test Scenario", "CM Test", "GreenAsJade");
    assertNotNull(created_scenario);
    assertEquals("Test Scenario", created_scenario.name);
    assertEquals("CM Test", created_scenario.game.name);
    assertNotNull(created_scenario.game.scenarios);

    Logger.info("checking that the scenario was added properly to the game...");

    Game retrieved_game = Game.find.where().eq("name", "CM Test").findUnique();
    assertNotNull("retrieved game", retrieved_game);
    assertNotNull("retrieved game scenarios", retrieved_game.scenarios);

    Logger.info("retrieving...");

    Scenario retrieved_scenario = Scenario.find.where().eq("name", "Test Scenario").findUnique();
    assertNotNull(retrieved_scenario);
    assertEquals("Test Scenario", retrieved_scenario.name);

    assertEquals("CM Test", retrieved_scenario.game.name);

    Logger.info("checking scenario's game's scenarios...");
    assertNotNull("retrived scenario's game's scenarios", retrieved_scenario.game.scenarios);
    assertFalse(retrieved_scenario.game.scenarios.isEmpty());
}

IE the last two asserts fail. But everything up till then seems to work the way I expect. All the log messages that you can see in there report "what you would expect" ...except that interestingly there is no Scenario constructor call associated with the find() operation, which does return a Scenario!

What am I doing wrong here?

Thanks!

Extra info:

  • The output of running the test, after applying the changes suggested by SpartanElite, with SQL logging on, is here: http://pastebin.com/BbMrCtZL

  • These are the exact source of ScenarioTest.java Scenario.java and Game.java that produced the results.

    -A tarball of the whole thing is here: http://bit.ly/onetomanytest. You should be able to extract it, cd to it, run play, and do test-only models.ScenarioTest to see the result. There is nothing in there other than this stuff and a User model.

役に立ちましたか?

解決 2

I found the answer, in the play frameworks google group. The correct incantation has to specifically ask for the Game's one-to-manys to be loaded like this:

Scenario retrieved_scenario = Scenario.find.fetch("game").where().eq("name", "Test Scenario").findUnique();

I don't properly understand what this is really saying - I haven't been able to find pointers to where I can read about how to use this Finder helper in this way. But it works.

他のヒント

What is happening is that you are not letting JPA know that the one-to-many relationship Game -< Scenario is a single relationship.

Currently you are not using the mappedBy attribute or the @JoinColumn annotation. So JPA thinks that both of these are Two Separate relationships with each other. So basically when you are finding a scenario using...

Scenario retrieved_scenario = Scenario.find.where().eq("name", "Test Scenario").findUnique();

It will find the scenario ("Test Scenario"), It will find the related game ("CM Test"). But when it tries to populate the fields for the game ("CM Test"), it has to a lookup in scenario table to find child scenarios. But since you have not defined the inverse side of the relationship it will be null.

So you can set the relationship to be a single bi-directional relationship by specifying the inverse side using...

@Entity
public class Game extends Model {

    @Id
    public String name;

    @OneToMany(mappedBy="game", cascade=CascadeType.ALL)
    public List<Scenario> scenarios;
}

@Entity
public class Scenario extends Model {

    @Id
    public String name;

    @ManyToOne
    @JoinColumn(name="gameId", nullable=false)
    public Game game;

Now when the Game ("CM Test") tries to do the lookup, it will know of the associated scenarios.

Also sometimes it helps to connect a debugging utility to the app. Then you can browse through the objects on JVP Heap Memory, and see there relationships.

[info] application - checking scenario's game's scenarios...
[debug] c.j.b.PreparedStatementHandle - select t0.name c0, t1.name c1, t1.gameId c2, t1.author_username c3 from game t0 left outer join scenario t1 on t1.gameId = t0.name  where t0.name = 'CM Test'   order by t0.name
[info] c.j.b.BoneCP - Shutting down connection pool...
[debug] c.j.b.PoolWatchThread - Terminating pool watch thread
[info] c.j.b.BoneCP - Connection pool has been shutdown.
[debug] c.j.b.BoneCPDataSource - Connection pool has been shut down
[info] models.ScenarioTest
[info] + createAndRetrieveScenario
[info] 
[info] 
[info] Total for test models.ScenarioTest
[info] Finished in 0.0010 seconds
[info] 1 tests, 0 failures, 0 errors

From your uploaded app using an H2 db configuration. The only thing I did was clean up the database unrelated issues that were preventing it from compiling; tests and models were unchanged.

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top