Android

Relationships on Android

Introduction

Using Parse, you can store data objects establishing relations between them. To model this behavior, any ParseObject can be used as a value in other ParseObject. Internally, the Parse framework will store the referred-to object in just one place, to maintain consistency. That can give you extra power when building and running complex queries. There are three main relation types:

  • one-to-one, establishing direct relations between two objects and only them;
  • one-to-many, where one object can be related to many other objects;
  • many-to-many, which can create many complex relations between many objects.

There are two ways to create a one-to-many relation in Parse:

  • The first is using the Pointers in Child Class, which is the fastest in creation and query time.
  • The second is using Arrays of Pointers in Parent Class which can lead to slow query times depending on their size. Because of this performance issue, we will use only pointers examples.

There are three ways to create a many-to-many relation in Parse.

  • The first is using the Parse Relations, which is the fastest in creation and query time. We will use this in this guide.
  • The second is using Arrays of Pointers which can lead to slow query times depending on their size.
  • The third is using JoinTable where the idea from classical database. When there is a many-to-nany relation, we combine every objectId or Pointer from both sides together to build a new separate table in which the relationship is tracked.

This tutorial uses a basic app created in Android Studio 4.1.1 with buildToolsVersion=30.0.2 , Compile SDK Version = 30.0.2 and targetSdkVersion 30

At any time, you can access the complete Project via our GitHub repositories.

Goal

Our goal is, understand Parse Relations by creating a practical Book app.

Here is a preview of what we are gonna achieve:

Prerequisites

To complete this tutorial, we need:

Understanding the Book App

The main object class you’ll be using is the Book class, storing each book entry in the registration. Also, these are the other three object classes:

  • Publisher: book publisher name, one-to-many relation with Book;
  • Genre: book genre, one-to-many relation with Book. Note that for this example we will consider that a book can only have one genre;
  • Author: book author, many-to-many relation with Book, since a book can have more than one author and an author can have more than one book as well;

A visual representation of these data model:

Let’s get started!

Before next steps, we need to connect Back4App to our application. You should save the appId and clientKey from the Back4App to string.xml file and then init Parse in our App.java or App.kt file.
Follow the New Parse App tutorial if you don’t know how to init Parse to your app.
Or you can download the projects we shared the github links above and edit only the appId and clientKey parts according to you.

In this step we will see how to save and list the Genres, Publishers and Authors classes related with the Book class.

Step 1.1 - Save and list Genres

We can register a Genre using the following snippet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void addGenre(String name) {
  //We are taking this name parameter from the input.
  progressDialog.show();
  ParseObject parseObject = new ParseObject("Genre");
  parseObject.put("name", name);
  parseObject.saveInBackground(e -> {
    progressDialog.dismiss();
    if (e == null) {
      getGenres();
      inputGenre.setText("");
      Toast.makeText(this, "Genre saved successfully", Toast.LENGTH_SHORT).show();
    } else { 
    Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private fun addGenre(name: String) {
  //We are taking this name parameter from the input.
  progressDialog.show()
  val parseObject = ParseObject("Genre")
  parseObject.put("name", name)
  parseObject.saveInBackground {
    progressDialog.dismiss()
    if (it == null) {
      getGenres()
      inputGenre.setText("")
      Toast.makeText(this, "Genre saved successfully", Toast.LENGTH_SHORT).show()
    } else {
        Toast.makeText(this, it.localizedMessage, Toast.LENGTH_SHORT).show()
    }
  }
}

We can list Genres using the following snippet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void getGenres() {
  progressDialog.show();
  ParseQuery<ParseObject> query = new ParseQuery<>("Genre");
  query.findInBackground((objects, e) -> {
    progressDialog.dismiss();
    List<ParseObjectModel> list = new ArrayList<>();
    for (ParseObject parseObject : objects) {
      list.add(new ParseObjectModel(parseObject));
    }

    GenreAdapter adapter = new GenreAdapter(list, this);
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    recyclerView.setAdapter(adapter);
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private fun getGenres() {
  progressDialog.show()
  val query = ParseQuery<ParseObject>("Genre")
  query.findInBackground { objects, e ->
  progressDialog.dismiss()
  var list: MutableList<ParseObjectModel> = ArrayList()
  for (parseObject in objects) {
    list.add(ParseObjectModel(parseObject))
  }
  val adapter = GenreAdapter(this, list)
  recyclerView.layoutManager = LinearLayoutManager(this)
  recyclerView.adapter = adapter
  }
}

Step 1.2 - Save and list Publishers

We can register a Publisher using the following snippet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void addPublisher(String name) {
  //We are taking this name parameter from the input.
  progressDialog.show();
  ParseObject parseObject = new ParseObject("Publisher");
  parseObject.put("name", name);
  parseObject.saveInBackground(e -> {
    progressDialog.dismiss();
    if (e == null) {
      getPublishers();
      inputPublisher.setText("");
      Toast.makeText(this, "Publisher saved successfully", Toast.LENGTH_SHORT).show();
    } else {
        Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private fun addPublisher(name: String) {
  //We are taking this name parameter from the input.
  progressDialog.show()
  val parseObject = ParseObject("Publisher")
  parseObject.put("name", name)
  parseObject.saveInBackground {
    progressDialog.dismiss()
    if (it == null) {
      getPublishers()
      inputPublisher.setText("")
      Toast.makeText(this, "Publisher saved successfully", Toast.LENGTH_SHORT).show()
    } else {
      Toast.makeText(this, it.localizedMessage, Toast.LENGTH_SHORT).show()
    }
  }
}

We can list Publishers using the following snippet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void getPublishers() {
  progressDialog.show();
  ParseQuery<ParseObject> query = new ParseQuery<>("Publisher");
  query.findInBackground((objects, e) -> {
    progressDialog.dismiss();
    List<ParseObjectModel> list = new ArrayList<>();
      for (ParseObject parseObject : objects) {
        list.add(new ParseObjectModel(parseObject));
      }
      
      PublisherAdapter adapter = new PublisherAdapter(list, this);
      recyclerView.setLayoutManager(new LinearLayoutManager(this));
      recyclerView.setAdapter(adapter);
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private fun getPublishers() {
  progressDialog.show()
  val query = ParseQuery<ParseObject>("Publisher")
  query.findInBackground { objects, e ->
    progressDialog.dismiss()
    val list: ArrayList<ParseObjectModel> = ArrayList()
    for (parseObject in objects) {
      list.add(ParseObjectModel(parseObject))
    }
  
    val adapter = PublisherAdapter(this, list)
    recyclerView.layoutManager = LinearLayoutManager(this)
    recyclerView.adapter = adapter
  }
}

Step 1.3 - Save and list Authors

We can register an Author using the following snippet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void addAuthor(String name){
  //We are taking this name parameter from the input.
  progressDialog.show();
  ParseObject parseObject = new ParseObject("Author");
  parseObject.put("name", name);
  parseObject.saveInBackground(e -> {
    progressDialog.dismiss();
    if (e == null) {
      getAuthors();
      inputAuthor.setText("");
      Toast.makeText(this, "Author saved successfully", Toast.LENGTH_SHORT).show();
    } else {
      Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private fun addAuthor(name: String) {
  //We are taking this name parameter from the input.
  progressDialog.show()
  val parseObject = ParseObject("Author")
  parseObject.put("name", name)
  parseObject.saveInBackground {
    progressDialog.dismiss()
    if (it == null) {
      getAuthors()
      inputAuthor.setText("")
      Toast.makeText(this, "Author saved successfully", Toast.LENGTH_SHORT).show()
    } else {
        Toast.makeText(this, it.localizedMessage, Toast.LENGTH_SHORT).show()
    }
  }
}

We can list Authors using the following snippet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void getAuthors() {
  progressDialog.show();
  ParseQuery<ParseObject> query = new ParseQuery<>("Author");
  query.findInBackground((objects, e) -> {
    progressDialog.dismiss();
    List<ParseObjectModel> list = new ArrayList<>();
    for (ParseObject parseObject : objects) {
      list.add(new ParseObjectModel(parseObject));
    }

    AuthorAdapter adapter = new AuthorAdapter(list, this);
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    recyclerView.setAdapter(adapter);
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private fun getAuthors() {
  progressDialog.show()
  val query = ParseQuery<ParseObject>("Author")
  query.findInBackground { objects: List<ParseObject>, e: ParseException? ->
    progressDialog.dismiss()
    val list: ArrayList<ParseObjectModel> = ArrayList()
    for (parseObject in objects) {
      list.add(ParseObjectModel(parseObject))
    }
    val adapter = AuthorAdapter(this, list)
    recyclerView.layoutManager = LinearLayoutManager(this)
    recyclerView.adapter = adapter
  }
}

In this part, we use a model class named ParseObjectModel. In this model class, we have a ParseObject variable to be able to read the data, and the isChecked variable, which we will use to save the book in the next step. We will be able to easily retrieve the selected objects with the isChecked variable.
Here is the our ParseObjectModel model.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ParseObjectModel {
  ParseObject object;
  boolean isChecked = false;
  
  public ParseObjectModel(ParseObject object) {
    this.object = object;
  }

  public ParseObject getObject() {
    return object;
  }

  public ParseObjectModel setObject(ParseObject object) {
    this.object = object;
    return this;
  }

  public boolean isChecked() {
    return isChecked;
  }

  public ParseObjectModel setChecked(boolean checked) {
    isChecked = checked;
    return this;
  }
}
1
2
3
4
5
6
7
8
class ParseObjectModel(obj: ParseObject) {
  var obj: ParseObject? = null
  var isChecked: Boolean = false
  
  init {
    this.obj = obj
  }
}

Step 2 - Save a Book object and its relations

Step 2.1 - Save a Book object with 1:N Relationship

This function will create a new Book in Back4app database with 1:N relations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
progressDialog.show();
book.put("genre", genre);
book.put("publisher", publisher);
book.put("title", title);
book.put("year", year);
book.saveInBackground(e -> {
  progressDialog.hide();
  if (e == null) {
    Toast.makeText(AddBookActivity.this, "Book saved successfully", Toast.LENGTH_SHORT).show();
    startActivity(new Intent(AddBookActivity.this, BookListActivity.class));
    finish();
  } else {
    Toast.makeText(AddBookActivity.this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
progressDialog.show()
book.put("genre", genre)
book.put("publisher", publisher!!)
book.put("title", title)
book.put("year", year)
book.saveInBackground {
  progressDialog.hide()
  if (it == null) {
    Toast.makeText(this, "Book saved successfully", Toast.LENGTH_SHORT).show()
    startActivity(Intent(this@AddBookActivity, BookListActivity::class.java))
    finish()
  } else {
    Toast.makeText(this, it.localizedMessage, Toast.LENGTH_SHORT).show()
  }
}

Step 2.2 Save a Book object with N:N Relationship

This function will create a new Book in Back4app database with N:N relations. For the Author relation, we find the selected Author/s in the adapter of the authorRecyclerView and save them as Parse Relation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
progressDialog.show();
book.put("genre", genre);
book.put("publisher", publisher);
book.put("title", title);
book.put("year", year);

//Here we are setting book relation with getSelectedItem function of BookAuthorAdapter.
if (recyclerViewAuthors.getAdapter() != null) {
  relation = ((BookAuthorAdapter) recyclerViewAuthors.getAdapter()).getSelectedItems(book);
    if (relation == null) {
      Toast.makeText(this, "Please select Author/s", Toast.LENGTH_SHORT).show();
      return;
    }
  } else {
    Toast.makeText(this, "Something went wrong!!", Toast.LENGTH_SHORT).show();
    return;
  }

book.saveInBackground(e -> {
  progressDialog.hide();
  if (e == null) {
    Toast.makeText(AddBookActivity.this, "Book saved successfully", Toast.LENGTH_SHORT).show();
    startActivity(new Intent(AddBookActivity.this, BookListActivity.class));
    finish();
  } else {
    Toast.makeText(AddBookActivity.this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
  }
});

//This is the function for save Author/s relation of Book object.This function in BookAuthorAdapter.
public ParseRelation<ParseObject> getSelectedItems(ParseObject parseObject) {
  ParseRelation<ParseObject> relation = parseObject.getRelation("author_relation");
  for (ParseObjectModel object : this.list) {
    if (object.isChecked())
    relation.add(object.getObject());
  }
  return relation;
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
progressDialog.show()
book.put("genre", genre)
book.put("publisher", publisher!!)
book.put("title", title)
book.put("year", year)

//Here we are setting book relation with getSelectedItem function of BookAuthorAdapter.

if (recyclerViewAuthors.adapter != null) {
  relation = (recyclerViewAuthors.adapter as BookAuthorAdapter).getSelectedItems(book)
  if (relation == null) {
    Toast.makeText(this, "Please select Author/s", Toast.LENGTH_SHORT).show()
    return
  }
} else {
  Toast.makeText(this, "Something went wrong!!", Toast.LENGTH_SHORT).show()
  return
}

book.saveInBackground {
  progressDialog.hide()
  if (it == null) {
    Toast.makeText(this, "Book saved successfully", Toast.LENGTH_SHORT).show()
    startActivity(Intent(this@AddBookActivity, BookListActivity::class.java))
    finish()
  } else {
    Toast.makeText(this, it.localizedMessage, Toast.LENGTH_SHORT).show()
  }
}

//This is the function for save Author/s relation of Book object.This function in BookAuthorAdapter.

fun getSelectedItems(parseObject: ParseObject): ParseRelation<ParseObject>? {
  var relation:ParseRelation<ParseObject>? = parseObject.getRelation("author_relation")
  for (obj in this.list) {
    if (obj.isChecked)
      relation?.add(obj.obj)
  }
  return relation
}

Step 3 - Query the Book Details with Relations

With these functions, we will list our Books according to their Publishers. First, we throw a query to the Publisher class.

1
2
3
4
5
6
7
8
9
10
11
12
progressDialog.show();
ParseQuery<ParseObject> query = new ParseQuery<>("Publisher");
query.findInBackground((objects, e) -> {
  progressDialog.hide();
  if (e == null) {
    BookListAdapter adapter = new BookListAdapter(objects, this);
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    recyclerView.setAdapter(adapter);
  } else {
    Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
progressDialog.show()
val query = ParseQuery<ParseObject>("Publisher")
query.findInBackground { objects: List<ParseObject>?, e: ParseException? ->
  progressDialog.hide()
  if (e == null) {
    val adapter = BookListAdapter(this, objects!!)
    recyclerView.layoutManager = LinearLayoutManager(this)
    recyclerView.adapter = adapter
  } else {
    Toast.makeText(this, e.localizedMessage, Toast.LENGTH_SHORT).show()
  }
}

And then we query to list the Books that each Publisher item is related to.

1
2
3
4
5
6
7
8
9
10
11
12
13
ParseObject object = list.get(position);
holder.title.setText(object.getString("name"));
ParseQuery<ParseObject> query = new ParseQuery<>("Book");
query.whereEqualTo("publisher", object);
query.findInBackground((objects, e) -> {
  if (e == null) {
    BooksAdapter adapter = new BooksAdapter(objects, context);
    holder.books.setLayoutManager(new LinearLayoutManager(context));
    holder.books.setAdapter(adapter);
  } else {
    Toast.makeText(context, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
val `object` = list[position]
holder.title.text = `object`.getString("name")
val query = ParseQuery<ParseObject>("Book")
query.whereEqualTo("publisher", `object`)
query.findInBackground { objects: List<ParseObject>?, e: ParseException? ->
  if (e == null) {
    val adapter = BooksAdapter(context, objects!!)
    holder.books.layoutManager = LinearLayoutManager(context)
    holder.books.adapter = adapter
  } else {
    Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show()
  }
}

Now, when we click on any Book object, we send the Object Id of this Book with an intent to the page that will show the details of that Book. And we get all the details of the Book from the database by using this Object Id on that page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void getBookWithDetails() {
  progressDialog.show();
  ParseQuery<ParseObject> query = new ParseQuery<>("Book");
  query.getInBackground(getIntent().getStringExtra("objectId"), (object, e) -> {
    if (e == null) {
      bookTitle.setText("Title: " +object.getString("title"));
      bookYear.setText("Year: " +object.getString("year"));
      try {
        bookGenre.setText("Genre: " +object.getParseObject("genre").fetchIfNeeded().getString("name"));
      } catch (ParseException parseException) {
          parseException.printStackTrace();
      }
      try {
        bookPublisher.setText("Publisher: " + object.getParseObject("publisher").fetchIfNeeded().getString("name"));
      } catch (ParseException parseException) {
        parseException.printStackTrace();
      }
      
      object.getRelation("author_relation").getQuery().findInBackground((objects, e1) -> {
        progressDialog.hide();
        if (e1 == null) {
          BookDetailAuthorAdapter adapter = new BookDetailAuthorAdapter(objects, this);
          authorRecyclerView.setLayoutManager(new LinearLayoutManager(this));
          authorRecyclerView.setAdapter(adapter);
        } else {
          Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
        }
      });
    } else {
      progressDialog.hide();
      Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private fun getBookWithDetails() {
  progressDialog.show()
  val query = ParseQuery<ParseObject>("Book")
  
  query.getInBackground(intent.getStringExtra("objectId")) { `object`, e ->
  if (e == null) {
    bookTitle.text = "Title: " + `object`.getString("title")
    bookYear.text = "Year: " + `object`.getString("year")
    try {
      bookGenre.text = "Genre: " + `object`.getParseObject("genre")?.fetchIfNeeded<ParseObject>()?.getString("name")
    } catch (parseException: ParseException) {
      parseException.printStackTrace()
    }
    try {
      bookPublisher.text =
      "Publisher: " + `object`.getParseObject("publisher")?.fetchIfNeeded<ParseObject>()?.getString("name")
    } catch (parseException: ParseException) {
      parseException.printStackTrace()
    }

    `object`.getRelation<ParseObject>("author_relation").query.findInBackground { objects, e1 ->
    progressDialog.hide()
      if (e1 == null) {
        val adapter = BookDetailAuthorAdapter(this, objects)
        authorRecyclerView.layoutManager = LinearLayoutManager(this)
        authorRecyclerView.adapter = adapter
      } else {
        Toast.makeText(this, e1.localizedMessage, Toast.LENGTH_SHORT).show()
      }
   }
  } else {
      progressDialog.hide()
      Toast.makeText(this, e.localizedMessage, Toast.LENGTH_SHORT).show()
    }
  }
}

It’s done!

At this point, we have learned Parse Relationships on Android.