Saving nested foreign objects with ORMLite on Android

23,457

Solution 1

As of version 4.27 ORMlite supports the foreignAutoCreate and foreignAutoRefresh settings on the @DatabaseField annotation on a field:

@DatabaseField(foreign = true, foreignAutoCreate = true, foreignAutoRefresh = true)
public Child child;

This means that you assign your child field and if the id field on the child is not set when the parent is created then it to will be created. The foreignAutoRefresh means that when a parent is retrieved a separate SQL call will be made to get the child field populated.

When doing this, the parent object is persisted but not the child object and the auto-generated child_id column in the parent table is set to 0. Is this normal behavior?

You can also have more control over when ORMLite makes the calls to the child object by creating the child before you create the parent.

Parent parent = new Parent();
parent.name = "ParentName";

Child child = new Child();
child.name = "ChildName";

parent.child = child;

// this will update the id in child
childDao.create(child);

// this saves the parent with the id of the child
parentDao.create(parent);

One more thing to note is that without the foreignAutoRefresh = true when you query for a Parent object, the child object that you get back only has its id field retrieved. If the id is an auto-generated int (for example), then the above name field will not be retrieved until you do an update on the child object.

// assuming the id of the Parent is the name
Parent parent = parentDao.queryForId("ParentName");
System.out.println("Child id should be set: " + parent.child.id);
System.out.println("Child name should be null: " + parent.child.name);

// now we refresh the child object to load all of the fields
childDao.refresh(parent.child);
System.out.println("Child name should now be set: " + parent.child.name);

For more documentation about this, see the online page about Foreign Object Fields.

Solution 2

Did you try this?

@DatabaseField(foreign = true, foreignAutoCreate = true, foreignAutoRefresh = true)
public Child child;

I'm using ORMLite 4.35.

Solution 3

@DatabaseField(foreign = true,foreignAutoCreate = true,foreignAutoRefresh = true)
public Child child;

Some Notes on this solution

  1. (foreignAutoCreate = true) work only if the ID field is not set (null or 0) according to ORMlite documentation http://ormlite.com/javadoc/ormlite-core/com/j256/ormlite/field/DatabaseField.html

    • foreignAutoCreate : "Set this to be true (default false) to have the foreign field will be automagically created using its internal DAO if the ID field is not set (null or 0)."
  2. This only works if generatedId is also set to true for the child table according to ORMlite documentation.

Solution 4

As mentioned, this does not seem to be supported in the lite version. I wrote a simple recursive function to save all referenced objects. I had problems getting the generics to play nice so in the end I just removed them all. I also made a base Entity class for my db objects.

So here is what I wrote. If anyone can get the same code to work with proper generics, or can improve upon it, please feel free to edit.

    // Debugging identity tag
    public static final String TAG = DatabaseHelper.class.getName();

    // Static map of common DAO objects
    @SuppressWarnings("rawtypes")
    private static final Map<Class, Dao<?, Integer>> sDaoClassMap = new HashMap<Class, Dao<?,Integer>>();

    /**
     * Persist an entity to the underlying database.
     * 
     * @param context
     * @param entity
     * @return boolean flag indicating success
     */
    public static boolean create(Context context, Entity entity) {
        // Get our database manager
        DatabaseHelper databaseHelper = DatabaseHelper.getHelper(context);

        try {
            // Recursively save entity
            create(databaseHelper, entity);

        } catch (IllegalArgumentException e) {
            Log.e(TAG, "Object is not an instance of the declaring class", e);
            return false;
        } catch (IllegalAccessException e) {
            Log.e(TAG, "Field is not accessible from the current context", e);
            return false;
        } catch (SQLException e) {
            Log.e(TAG, "Unable to create object", e);
            return false;
        }

        // Release database helper
        DatabaseHelper.release();

        // Return true on success
        return true;
    }

    /**
     * Persist an entity to the underlying database.<br><br>
     * For each field that has a DatabaseField annotation with foreign set to true, 
     * and is an instance of Entity, recursive attempt to persist that entity as well. 
     * 
     * @param databaseHelper
     * @param entity
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     * @throws SQLException
     */
    @SuppressWarnings("unchecked")
    public static void create(DatabaseHelper databaseHelper, Entity entity) throws IllegalArgumentException, IllegalAccessException, SQLException {
        // Class type of entity used for reflection
        @SuppressWarnings("rawtypes")
        Class clazz = entity.getClass();

        // Search declared fields and save child entities before saving parent. 
        for(Field field : clazz.getDeclaredFields()) {
            // Inspect annotations
            for(Annotation annotation : field.getDeclaredAnnotations()) {
                // Only consider fields with the DatabaseField annotation
                if(annotation instanceof DatabaseField) {
                    // Check for foreign attribute
                    DatabaseField databaseField = (DatabaseField)annotation;
                    if(databaseField.foreign()) {
                        // Check for instance of Entity
                        Object object = field.get(entity);                      
                        if(object instanceof Entity) {
                            // Recursive persist referenced entity
                            create(databaseHelper, (Entity)object);
                        }
                    }
                }
            }
        }

        // Retrieve the common DAO for the entity class
        Dao<Entity, Integer> dao = (Dao<Entity, Integer>) sDaoClassMap.get(clazz);
        // If the DAO does not exist, create it and add it to the static map
        if(dao == null) {
            dao = BaseDaoImpl.createDao(databaseHelper.getConnectionSource(), clazz);
            sDaoClassMap.put(clazz, dao);
        }

        // Persist the entity to the database
        dao.create(entity);
    }
Share:
23,457
Chase
Author by

Chase

Android/iOS developer in Tokyo. Good times.

Updated on October 13, 2020

Comments

  • Chase
    Chase over 3 years

    When working on Android, does ORMLite only save shallow level objects? I have a data structure with nested Objects, both of which are newly created, and I would like to be able to save both of them with one call to dao.create()

    For exmaple, I have the following Parent Class.

    @DatabaseTable
    public class Parent {
    
      @DatabaseField(generatedId=true)
      public int id;
    
      @DatabaseField
      public String name;
    
      @DatabaseField
      public Child child;
    }
    

    and the following Child Class.

    @DatabaseTable
    public class Child {
    
      @DatabaseField(generatedId=true)
      public int id;
    
      @DatabaseField
      public String name;
    }
    

    I want to be able to do the following.

    Parent parent = new Parent();
    parent.name = "ParentName";
    
    Child child = new Child();
    child.name = "ChildName";
    
    parent.child = child;
    
    //  .. get helper and create dao object...
    dao.create(parent);
    

    When doing this, the parent object is persisted but not the child object and the auto-generated child_id column in the parent table is set to 0. Is this normal behavior? Is there a way to have nested objects persisted and propagate the primary key up?

  • Chase
    Chase over 13 years
    Thanks for the feedback. I figured as much. My question only gave an simple example, but what I really would like to do is be able to store rather complex objects without having to know too much about the object themselves. I wrote up a quick solution which I will post, but I think it can be improved upon.
  • Gray
    Gray over 13 years
    Thanks for this dude. Yeah, I've been considering adding the ability for a foreign field to be auto created/updated. It's not going to be the default however and it will be in another database transaction as opposed to magic joins like hibernate uses.
  • Gray
    Gray over 13 years
    Actually, after thinking about it some more, typically the child has the parent as a foreign object. The Account exists before the Order object that has a foreign Account field exists. Typically the auto-refresh of the object is possible but the auto-create would be strange. Hrm.
  • Chase
    Chase over 13 years
    My understanding is that the only auto generated columns are those for foreign objects, meaning that child objects do not get auto created references to their parent. With that in mind, the above code is working fine for me, although I have since slightly improved the generics. In the end, I am using GSON to map JSON to an object and using the same object to persist locally using ORM Lite. I had to write a few custom DAO objects to load and create List entities (one to many relationships.) It's working great but is a little slow. It would be nice to make this a built in feature :)
  • Gray
    Gray about 13 years
    Please do post it @Chase to the ORMLite mailing list. groups.google.com/group/ormlite-user
  • Chase
    Chase about 13 years
    Sure. I noticed that the newer version of ORMLite now supports a similar feature. I have since made many other enhancements that I will post the mailing list too.
  • Bahadır Yıldırım
    Bahadır Yıldırım over 11 years
    My previous comment stated that ORMLite for Android doesn't support these features, but it does!
  • bentzy
    bentzy over 10 years
    +1 - because this is the right answer in my opinion. This almost worked for me. My problem is that I get the foreign object with ID from the server and it only auto create foreign objects with id=0 or null :( What can I do to avoid manually creating the foreign object?
  • Alexander Malakhov
    Alexander Malakhov over 10 years
    Note this approach could impact performance. For example, in UI you may show few info in object header and query for detailed info only on user demand.
  • Tyler
    Tyler about 10 years
    Does not work with @ForeignCollectionField, unforunately
  • Kalpesh
    Kalpesh over 8 years
    Its worked well and my child object also create when I try to create parent object.But When I change value of one of field in child object and then I call update parent object then its not update child object automatically. Have you any clue for that ?
  • murt
    murt about 8 years
    @gray According to ORMLite documentation you should do also parent.child = child; and child=parent; .Moreover whan I used on create ParentDao after ChilderDao so childrenDao.create(children); parentDao.create(parent); it causes that children are created twice. May it be the issue that Collection<Children> has eager=true ?
  • Rauter
    Rauter about 8 years
    Is there a simple solution like this if 'generatedId = false'?
  • Gray
    Gray about 8 years
    It would work with id = true but I think that there needs to be some id column for it to work @Rauter.
  • lostintranslation
    lostintranslation almost 8 years
    This doesn't work if the child has a reference back to the parent that has canBeNull = false. Since trying to create the child first without the parent reference will fail.
  • Gray
    Gray almost 8 years
    If the parent has a reference to the child then the child can't have a reference back to the parent @lostintranslation. One has to be created first is the point.