15

I'm trying to design and implement a folder tree structure in Android SQLite with help of Android Room Persistence (an ORM) which Google introduced in I/O 2017. In my design, a folder can contain another folders and files. Here are my codes for folder and file:

File Model:

@Entity(tableName = "files", foreignKeys = @ForeignKey(entity = Folder.class,
    parentColumns = "id",
    childColumns = "parent_id",
    onDelete = CASCADE))
public class File {
    @PrimaryKey(autoGenerate = true)
    private int id;

    private String title;
    private Date creationDate;

    @ColumnInfo(name = "parent_id")
    public int parentId;

    //here setters and getters skipped but exist in original code
}

And Here is Folder Code:

@Entity(tableName = "folders", foreignKeys = @ForeignKey(entity = Folder.class,
    parentColumns = "id",
    childColumns = "parent_id",
    onDelete = CASCADE,onUpdate = SET_NULL))
public class Folder {
    @PrimaryKey(autoGenerate = true)
    private int id;

    private String name;

    @ColumnInfo(name = "parent_id")
    private int parentId;
}

Which have a foreign key on ID column of it self for parent.

and here is FolderDAO:

@Dao
public interface FolderDAO {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insertFolder(Folder... folders);

    @Update
    public void updateFolder(Folder... folders);


    @Delete
    public void deleteFolders(Folder... folders);

    @Query("SELECT * FROM folders")
    List<Folder> getAll();

    @Query("SELECT * FROM folders WHERE id IN (:folderIds)")
    List<Folder> loadAllByIds(int[] folderIds);
}

But when I make a folder object and try to insert it:

AsyncTask.execute(new Runnable() {
        @Override
        public void run() {
            Folder folder = new Folder();
            folder.setName("All");

            DatabaseInstance.getInstance(getApplicationContext()).folderDAO().insertFolder(folder);

        }
    });

get this error:

FOREIGN KEY constraint failed

         --------- beginning of crash
E/AndroidRuntime: FATAL EXCEPTION: AsyncTask #1
              Process: sahraei.hamidreza.com.note, PID: 1835
              android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787)
                  at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
                  at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:782)
                  at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:788)
                  at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:86)
                  at android.arch.persistence.db.framework.FrameworkSQLiteStatement.executeInsert(FrameworkSQLiteStatement.java:80)
                  at android.arch.persistence.room.EntityInsertionAdapter.insert(EntityInsertionAdapter.java:80)
                  at sahraei.hamidreza.com.note.DAO.FolderDAO_Impl.insertFolder(FolderDAO_Impl.java:80)
                  at sahraei.hamidreza.com.note.ItemListActivity$1.run(ItemListActivity.java:62)
                  at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
                  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
                  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
                  at java.lang.Thread.run(Thread.java:818)

Does anyone know what's wrong or suggest another design for my project table?

Hamidreza Sahraei
  • 457
  • 1
  • 5
  • 13
  • You have not specified a parent folder, and my guess is that is what is triggering this error. Of course, you *can't* specify a parent folder for the first folder, as there is no parent to point to. [Self-referential foreign keys work in SQLite](https://stackoverflow.com/q/15967146/115145), though it looks like [you need to explicitly use `NULL` for the value when it is missing](https://stackoverflow.com/a/6518551/115145). I do not know what Room is generating for the `INSERT` statement here. – CommonsWare Jun 19 '17 at 15:40
  • @CommonsWare I have tried NULL value, also I put an integer for parent column for example 0, but the result not changed and still getting this error. – Hamidreza Sahraei Jun 19 '17 at 16:59
  • 1
    @HamidrezaSahraei: Pls, have a look at this. https://stackoverflow.com/q/44749812/1788806 Using String as datatype is unnecessery for your use case. you need to use Integer, because this is a class (not a primitive) which can be null. – Willi Mentzel Jun 28 '17 at 12:36
  • Did you update your database model without doing a clean install ? Also do you have auto backup enabled in your manifest ?? – Jaswanth Manigundan Aug 10 '17 at 01:32

2 Answers2

10

I got this to work, but not using int primary keys. I'm not a huge fan of those for this sort of ORM scenario, just because of these sorts of problems.

So, here's a self-referential Category class that uses a UUID for its primary key:

/***
 Copyright (c) 2017 CommonsWare, LLC
 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 com.commonsware.android.room.dao;

import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Ignore;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import java.util.UUID;
import static android.arch.persistence.room.ForeignKey.CASCADE;

@Entity(
  tableName="categories",
  foreignKeys=@ForeignKey(
    entity=Category.class,
    parentColumns="id",
    childColumns="parentId",
    onDelete=CASCADE),
  indices=@Index(value="parentId"))
public class Category {
  @PrimaryKey
  public final String id;
  public final String title;
  public final String parentId;

  @Ignore
  public Category(String title) {
    this(title, null);
  }

  @Ignore
  public Category(String title, String parentId) {
    this(UUID.randomUUID().toString(), title, parentId);
  }

  public Category(String id, String title, String parentId) {
    this.id=id;
    this.title=title;
    this.parentId=parentId;
  }
}

Now you can have DAO methods like:

@Query("SELECT * FROM categories WHERE parentId IS NULL")
Category findRootCategory();

@Query("SELECT * FROM categories WHERE parentId=:parentId")
List<Category> findChildCategories(String parentId);
CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • 1
    This is great, I used your code to implement folder structure and now it's working like a charm. But do you know what was the problem with my code? – Hamidreza Sahraei Jun 22 '17 at 09:02
  • 3
    @HamidrezaSahraei: The only significant difference is that you are using auto-generated `int` keys, and I am not. You could try switching to `Integer` and see if a `null` default value works better. My guess is that the `0` default `int` broke the foreign key constraint, because Room cannot tell that `0` means "no relation". But, that's just a guess. – CommonsWare Jun 22 '17 at 10:56
  • 1
    You can make your foreign key type Integer, which is nullable. And keep your id type int – Yushi Aug 21 '17 at 20:30
  • 1
    @Yushi: Yes, as of `1.0.0-alpha8` or so. That feature did not exist at the time I wrote this answer. – CommonsWare Aug 21 '17 at 20:31
  • @CommonsWare I'm facing issues while updating, it won't throw any exceptions. but the table is not updating. Let say you need to move the item from one folder to another, basically, you are changing the parent. if you do that, it was not working. Won't see any exception also. Is that is a problem with self-referential tables? Please help!! How to resolve that – sreekumar Oct 10 '17 at 15:18
  • @sreekumar: I recommend that you post your own Stack Overflow question with a [mcve] demonstrating what you are trying and what "not working" means. – CommonsWare Oct 10 '17 at 15:23
  • @CommonsWare posted, can you help me with this. https://stackoverflow.com/questions/46691663/update-failing-on-self-referential-foreign-key-with-room-update-method – sreekumar Oct 11 '17 at 15:04
5

It took me a loong time to figure out that having a parentColumns element which is an autogenerated primary key does not seem to be allowed at all, even if it is a uuid using a Kotlin data class with a default value like this:

class MyClass {
    // Don't do this
    @PrimaryKey(autoGenerate = true)
    var uuid: String = UUID.randomUUID()
}

Always make sure the parent column is specified directly by you.

Mahozad
  • 18,032
  • 13
  • 118
  • 133
Daniel Wilson
  • 18,838
  • 12
  • 85
  • 135
  • 4
    It seems that when you use `autoGenerate = true`, room uses the `rowId` column as the primary key. But sqlite does not allow FOREIGN KEY on a rowId. The FOREIGN KEY must target a named column. https://www.sqlite.org/foreignkeys.html That would explain why you can't have a foreign key targetting a column which is auto generated. – JDenais Jun 11 '19 at 10:20
  • @JDenais Sure you can use auto generated columns as foreign key. You just can't directly use rowid as key. – The incredible Jan Jan 13 '21 at 13:23
  • @Daniel Wilson You first order autoGenerate and then set the value by yourself anyway? Doesn't make any sense to me. https://developer.android.com/reference/android/arch/persistence/room/PrimaryKey – The incredible Jan Jan 13 '21 at 13:27