1

I am creating an app that requires a team of players. I am using the Team ID as the team primary key and the foreign key for each player. In one fragment I create a new team. When the team is created and added to my room database it initially has the ID of 0 or not set even though I have auto generate set to true. I then navigate to the team roster view which has the ability to add new players to the team. When I create a new player and use the new teams ID in the team view model the team ID is still 0 or not set so the app crashes and there is a foreign key restraint failure. After the crash if I reopen the app or if I avoid the crash by going back to the team list and selecting the team that was just created with an initial id of 0, when I create a player this time the team will have a valid ID. Why does room not immediately assign a unique ID when the object is created and it waits for navigation away and back to the fragment or a restart of the app? Relevant code below, feels like I might be giving too code much but I am following jetpack best practices that I found from android documentation, and I do not know where the problem is stemming from. https://developer.android.com/jetpack/docs/guide.

Database

@Database (entities = {Team.class,
                   Player.class},
       version = 6)
public abstract class AppDatabase
    extends RoomDatabase
{
private static final String DATABASE_NAME = "Ultimate_Stats_Database";
private static volatile AppDatabase instance;

public abstract TeamDAO teamDao ();
public abstract PlayerDAO playerDAO ();

static synchronized AppDatabase getInstance (Context context)
{
    if (instance == null)
    {
        // Create the instance
        instance = create(context);
    }

    // Return the instance
    return instance;
}

private static AppDatabase create (final Context context)
{
    // Create a new room database
    return Room.databaseBuilder(
                context,
                AppDatabase.class,
                DATABASE_NAME)
               .fallbackToDestructiveMigration()    // TODO Add migrations, poor practice to ignore
               .build();
}
}

Team entity

@Entity (tableName = "teams")
public class Team
    implements Parcelable
{
@PrimaryKey (autoGenerate = true)
private long id;
private String name;


public Team ()
{
    this.name = "";
}


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

Team DAO

@Dao
public abstract class TeamDAO
{

@Insert (onConflict = OnConflictStrategy.REPLACE)
public abstract long insert (Team team);


@Delete
public abstract int deleteTeam (Team team);


@Query ("SELECT * FROM teams")
public abstract LiveData<List<Team>> getAllTeams ();
}

Team Repository (Inserting Only)

private TeamDAO teamDao;
private LiveData<List<Team>> teams;

public TeamRepository (Application application)
{
    AppDatabase db = AppDatabase.getInstance(application);
    teamDao = db.teamDao();
    teams = teamDao.getAllTeams();
}

private static class insertAsyncTask
        extends AsyncTask<Team, Void, Void>
{

    private TeamDAO asyncTeamTaskDao;


    insertAsyncTask (TeamDAO teamDao)
    {
        asyncTeamTaskDao = teamDao;
    }


    @Override
    protected Void doInBackground (final Team... params)
    {
        // Trace entry
        Trace t = new Trace();

        // Insert the team into the database
        asyncTeamTaskDao.insert(params[0]);

        // Trace exit
        t.end();

        return null;
    }
}

Team View Model

public class TeamViewModel
    extends AndroidViewModel
{
private TeamRepository teamRepository;
private LiveData<List<Team>> teams;
private MutableLiveData<Team> selectedTeam;

public TeamViewModel (Application application)
{
    super(application);
    teamRepository = new TeamRepository(application);
    teams = teamRepository.getAllTeams();
    selectedTeam = new MutableLiveData<Team>();
}

public LiveData<Team> getSelectedTeam()
{
    return selectedTeam;
}

public void selectTeam(Team team)
{
    selectedTeam.setValue(team);
}

public LiveData<List<Team>> getTeams ()
{
    return teams;
}

public void insert (Team team)
{
    teamRepository.insert(team);
}
...

Player entity

@Entity(tableName = "players",
        foreignKeys = @ForeignKey(entity = Team.class,
                              parentColumns = "id",
                              childColumns = "teamId"),
        indices = {@Index(value = ("teamId"))})
public class Player
    implements Parcelable
{

@PrimaryKey (autoGenerate = true)
private long id;
private String name;
private int line;
private int position;
private long teamId;

public Player ()
{
    this.name = "";
    this.line = 0;
    this.position = 0;
    this.teamId = 0;
}


public Player(String name,
              int line,
              int position,
              long teamId)
{
    this.name = name;
    this.line = line;
    this.position = position;
    this.teamId = teamId;
}
....

Player DAO

@Dao
public abstract class PlayerDAO
{

@Insert (onConflict = OnConflictStrategy.REPLACE)
public abstract void insert (Player player);


@Delete
public abstract int deletePlayer (Player player);


@Query ("SELECT * FROM players WHERE teamId = :teamId")
public abstract LiveData<List<Player>> getPlayersOnTeam (long teamId);


@Query ("SELECT * FROM players")
public abstract LiveData<List<Player>> getAllPlayers();


@Query ("SELECT * FROM players WHERE id = :id")
public abstract LiveData<Player> getPlayerById (long id);
}

Player Repository (Inserting Only)

private PlayerDAO playerDAO;
private LiveData<List<Player>> players;

public PlayerRepository(Application application)
{
    AppDatabase db = AppDatabase.getInstance(application);
    playerDAO = db.playerDAO();
    players = playerDAO.getAllPlayers();
}

public void insert (Player player)
{
    new PlayerRepository.insertAsyncTask(playerDAO).execute(player);
}

private static class insertAsyncTask
        extends AsyncTask<Player, Void, Void>
{
    private PlayerDAO asyncTaskDao;

    insertAsyncTask (PlayerDAO dao)
    {
        asyncTaskDao = dao;
    }

    @Override
    protected Void doInBackground (final Player... params)
    {
        // Get the player being inserted by its id
        LiveData<Player> player = asyncTaskDao.getPlayerById(((Player) params[0]).getId());

        if (player != null)
        {
            // Delete the old record of the player
            asyncTaskDao.deletePlayer(params[0]);
        }

        // Insert the player into the database
        asyncTaskDao.insert(params[0]);

        return null;
    }
}
...

Player View Model

public class PlayerViewModel
    extends AndroidViewModel
{
private PlayerRepository playerRepository;
private LiveData<List<Player>> players;
private MutableLiveData<Player> selectedPlayer;

public PlayerViewModel(Application application)
{
    super(application);
    playerRepository = new PlayerRepository(application);
    players = playerRepository.getAllPlayers();
    selectedPlayer = new MutableLiveData<Player>();
}

public LiveData<Player> getSelectedPlayer()
{
    return selectedPlayer;
}

public void selectPlayer(Player player)
{
    selectedPlayer.setValue(player);
}

public LiveData<List<Player>> getPlayers ()
{
    return players;
}

public void insert (Player player)
{
    playerRepository.insert(player);
}
...

Where I create the team (in the TeamListFragment and when a dialog fragment is completed)

public void onDialogPositiveClick (String teamName)
{
    // Trace entry
    Trace t = new Trace();

    // Create a new team object
    Team newTeam = new Team();

    // Name the new team
    newTeam.setName(teamName);

    // Insert the team into the database and set it as the selected team
    teamViewModel.insert(newTeam);
    teamViewModel.selectTeam(newTeam);

    // Trace exit
    t.end();

    // Go to the player list view
    routeToPlayerList();
}

In the playerListFragment when it is created

    /*------------------------------------------------------------------------------------------------------------------------------------------*
     *  If the view model has a selected team                                                                                                   *
     *------------------------------------------------------------------------------------------------------------------------------------------*/
    if (sharedTeamViewModel.getSelectedTeam().getValue() != null)
    {
        // Set the team to the team selected
        team = sharedTeamViewModel.getSelectedTeam().getValue();

        // Set the team name fields default text
        teamNameField.setText(team.getName());
    }

In the playerFragment when the save button is clicked

        @Override
        public void onClick (View v)
        {
            // Trace entry
            Trace t = new Trace();

            // Update the player object with the info given by the user
            boolean success = getUserInput();

            /*------------------------------------------------------------------------------------------------------------------------------*
             *  If the input was valid                                                                                                      *
             *------------------------------------------------------------------------------------------------------------------------------*/
            if (success)
            {
                // Set the player id to the team that is selected
                player.setTeamId(sharedTeamViewModel.getSelectedTeam()
                                                    .getValue()
                                                    .getId());

                // Input the the player into the player view model
                sharedPlayerViewModel.insert(player);

                // Remove this fragment from the stack
                getActivity().onBackPressed();
            }

            // Trace exit
            t.end();
        }

If there is any other code that is needed let me know

AlexCawley
  • 27
  • 1
  • 8
  • 1
    Insert always returns a long value in room database. Create the return type of insert to long and try storing it in long variable where you are inserting the data and try checking what the value is. The insert function returns a long value by default. – Kartik Feb 16 '19 at 22:11

1 Answers1

1

This is an expected behavior. Room will not directly update the id field in the newTeam.

It does not make sense for Room to change the input object, not to mention that Room does not assume that the entity fields are mutable. You can make all your Entity fields immutable and I believe it is a good practice to make your entity classes immutable whenever possible.

If you want to retrieve the id of the inserted row, check out this SO link: Android Room - Get the id of new inserted row with auto-generate

Sanlok Lee
  • 3,404
  • 2
  • 15
  • 25
  • Thank you,your answer along with Kartik's comment led me to the solution. I was thinking that room generated the ID as soon as the object was created and did not realize I was still responsible for ensuring an ID is assigned. To do this I followed Kartik and my DAO, Team Repository, and Team View Model all return longs that I use in the Fragment to assign to the ID of the Team I create. – AlexCawley Feb 17 '19 at 20:05