Realm: Get objects with selection on list properties - ios

My Model Classes (shortened):
class Customer: RealmSwift.Object {
let orders = List<Order>()
}
class Order: RealmSwift.Object {
#objc dynamic var areaCode: String? = nil
#objc dynamic var isPaid: Bool = false
}
Now I want to get all Customers with non paid orders in a given area.
I use this query to get them (I tested this query using Realm Studio):
orders.areaCode == '5429' and orders.isPaid == false
But with this Query I don't get only customers with non paid orders in '5429'.
I also get customers with orders in '5429' and paid orders in another area.
But I get only customers with both parts, but even in different rows.
How can I change my query to get only "customers with non paid orders in '5429'"?

You can use a subquery to find all Customers whose orders property contains at least one Order, where the areaCode is "5429" and isPaid is false.
let areaCode = "5429"
let nonPaidInArea = realm.objects(Customer.self).filter("SUBQUERY(orders, $order, $order.areaCode == %# AND $order.isPaid == false).#count>0",areaCode)

Related

Filtering Realm with nested subqueries

My app has data that looks like this.
class ShelfCollection: Object {
let shelves: List<Shelf>
}
class Shelf: Object {
let items: List<Item>
}
class Item: Object {
var name: String
let infos: List<String>
}
I'm trying to get all shelves in a shelf collection where any items match the query either by name or by an element in their infos list. From my understanding this predicate should be correct, but it crashes.
let wildQuery = "*" + query + "*"
shelfResults = shelfCollection.shelves.filter(
"SUBQUERY(items, $item, $item.name LIKE[c] %# OR SUBQUERY($item.infos, $info, info LIKE[c] %#).#count > 0).#count > 0",
wildQuery, wildQuery
)
It complies as a NSPredicate, but crashes when Realm is attempting to parse it, throwing me
'RLMException', reason: 'Object type '(null)' not managed by the Realm'
I suspect the nested subquery might be what fails, but I don't know enough about NSPredicate to be sure. Is this an acceptable query, and how can I make it.. work?
This is an answer and a solution but there's going to be a number of issues with the way the objects are structured which may cause other problems. It was difficult to create a matching dataset since many objects appear within other objects.
The issue:
Realm cannot currently filter on a List of primitives
EDIT: Release 10.7 added support for filters/queries as well as aggregate functions on primitives so the below info is no longer completely valid. However, it's still something to be aware of.
so this Item property will not work for filtering:
let infos: List<String>
However, you can create another object that has a String property and filter on that object
class InfoClass: Object {
#objc dynamic var info_name = ""
}
and then the Item class looks like this
class Item: Object {
var name: String
let infos = List<InfoClass>()
}
and then you filter based on the InfoClass object, not it's string property. So you would have some objects
let info0 = InfoClass()
info0.info_name = "Info 0 name"
let info1 = InfoClass()
info1.info_name = "Info 1 name"
let info2 = InfoClass()
info2.info_name = "Info 2 name"
which are stored in the Item->infos list. Then the question
I'm trying to get all shelves in a shelf collection...
states you want to filter for a collection, c0 in this case, shelves whose items contain a particular info in their list. Lets say we want to get those shelves whose items have info2 in their list
//first get the info2 object that we want to filter for
guard let info2 = realm.objects(InfoClass.self).filter("info_name == 'Info 2 name'").first else {
print("info2 not found")
return
}
print("info2 found, continuing")
//get the c0 collection that we want to get the shelves for
if let c0 = realm.objects(ShelfCollection.self).filter("collection_name == 'c0'").first {
let shelfResults = c0.shelves.filter("ANY items.infoList == %#", info2)
for shelf in shelfResults {
print(shelf.shelf_name)
}
} else {
print("c0 not found")
}
I omitted filtering for the name property since you know how to do that already.
The issue here is the infos could appear in many items, and those items could appear in many shelf lists. So because of the depth of data, with my test data, it was hard to have the filter return discreet data - it would probably make more sense (to me) if I had example data to work with.
Either way, the answer works for this use case, but I am thinking another structure may be better but I don't know the full use case so hard to suggest that.

Group chat - How to populate a table view with only the chat rooms a user is involved in?

I'm trying to make a group chat app, and so far I have the app setup (from a tutorial) so that any user can create a room, and all of those rooms show up in a central list, which is the same for all users. Now I need to alter this so that users only see rooms that they created, or have been added to (just like any messaging app like iMessage).
This is how a room is created and appended to the Room object (and uploaded to Firebase):
var rooms: [Room] = []
// Create new room
#IBAction func createRoom(_ sender: Any) {
if let name = newRoomTextField?.text {
let newRoomRef = roomRef.childByAutoId()
let roomItem = [
"name": name
]
newRoomRef.setValue(roomItem)
}
}
private func observeRooms() {
// Observe method to listen for new channels being written to Firebase
roomRefHandle = roomRef.observe(.childAdded, with: { (snapshot) -> Void in
let roomData = snapshot.value as! Dictionary<String, AnyObject>
let id = snapshot.key
if let name = roomData["name"] as! String!, name.characters.count > 0 {
self.rooms.append(Room(id: id, name: name))
self.tableView.reloadData()
} else {
print("Error! Could not decode channel data")
}
})
}
Room object:
internal class Room {
internal let id: String
internal let name: String
init(id: String, name: String) {
self.id = id
self.name = name
}
}
Currently the table view of rooms is populated with cell.textLabel?.text = rooms[(indexPath as NSIndexPath).row].name (cellForRow method), and return rooms.count (numberOfRows method), thus returning all of the rooms in the app, not specific to the user.
This is where I'm not sure how to proceed: I'm guessing I'll add an array "participants: [String]" to my Room object (in addition to "id" and "name"), then in observeRooms when I self.rooms.append(Room(id: id, name: name)), I'll also append the user's id to "participants"? Then populate the table view with only the rooms that the current user's id is a participant in?
Is this the right approach?
Thanks for any help/guidance!
I think this is the right approach, you basically have 2 choices :
Store a participants array into each room, which allow you to query rooms where this array contains our current userId.
Store a rooms array on each user, allowing to query rooms directly from this data.
The first approach seems to make more sense in case you'd like to also display a list of participants, though the query may take longer to execute than the second approach for many rooms * many participants.
You could also use both approach at the same time to have both your functionality and a faster query time for the user rooms.

Best practice to retrieve list of friends information. How to detect multiple queries are finished?

I have users structure lke this:
{
"users": {
"uniqueID1": {
"name": "Anon",
"friends": {
"uniqueID2": true,
"uniqueID3": true
}
}
"uniqueID2": { },
"uniqueID3": { },
}
}
I want to show a user's friends' names. I have to access $user/friends/ to get list of unique IDs, and iterate the list to get friend's information. But iterating the unique ID is making multiple queries, and I have to always check if all of my queries are finished. According to the doc, it seems multiple queries will not impact the performance too much, but if I want to update my view only when all of the queries are finished, I have to check how many queries are finished.
Is there no way of 'execute a completion block when all queries are finished'?
Pseudocode
var totalNumOfFriends = 0
var tempArray = NewArray()
ref(/users/uniqueID1/friends).observeEventType{ snapshot
var uIDList = snapshot.children's keys
totalNumOfFriends = uIDList .count
for uID in uIDList {
var nameRef = ref(/users/uID/name) i.e. /users/uniqueID3/name
nameRef.observeSingleEventOfType { snapshot
var username = snapshot.value
tempArray.append(username)
if tempArray.count == totalNumOfFriends {
// If counts are the same, tempArray has all of my friends' names
// Now update view using tempArray
}
}
}
}
Pseudocode explanation:
Get list of unique IDs from /users/uniqueID1/friends
'Save' number of unique IDs. (Explained in step 4)
For each unique IDs from the list, get user's name by using ref like this /users/uniquedID2/name
For each name retrieved, add it to temporary array. Once the count of the temporary array equals to the count from step 2, update my view as I have retrieved all the names.
Firebase has no built-in way to signal when a number of queries has finished. But you can easily implement this in your own code. Your approach with a counter that checks how many items have already been loaded is the most common approach for that.

Parse does not let me create 4 rows of information for same user sign up

I am creating a Sign Up Table on Swift using Parse to store data. Users can select maximum of 4 activities from the given list of activities.
I want the table to store all the 4 records of selected separately. But the code only makes one record!
Here is the code snippet-
//Creating a NSMutableSet to avoid multiple selection of same sport.
var tempSport = NSMutableSet()
//The vales in tempSport are : ["Activity1", "Activity2", "Activity3", "Activity4"]
#IBAction func getStartedButton(sender: AnyObject) {
if tempSport.count < 1
{
//Display Alert
}
else
{
var sendSport = Array(tempSport.allObjects)
print(sendSport)
for sport in sendSport
{
SportList["SportPlayer"] = user.username
SportList["SportPlaying"] = String(sport)
SportList.saveInBackground()
}
}
}
Image Link. Click here to see the expected output and output produced
Just save the object with an Array instead of 4 individual objects
let object = PFObject(className: "Your Class Name"
object["Sport Playing"] = [activity1, activity2, activity3, activity4]
object.saveInBackground()
Hope this helps.
Are you sure that you want to create a up to four sport objects for each user? That could end up becoming a lot of data in your database. I would recommend creating a a list of sports in the Parse database (soccer, football, tennis, etc) and create an array of users for each sport. For example, you would have Soccer under SportPlaying and an array of user objectIds under SportPlayer. Lets say you have a user with an objectId of H67c0uTUO1 that says they play Soccer, Basketball, and Lacrosse. You could send this data up to Parse by querying for the name of each sport that the user plays and adding the user's objectId to to the array using .addObject. Then, if you want a list of users who play that certain sport, you would query for using:
let sportQuery = PFQuery(className: "_User")
sportQuery.whereKey("objectId", containedIn: yourArrayFromSportPlayerForSpecificSport as! [anyObject])
let query = PFQuery.orQueryWithSubqueries([sportQuery])
query.findObjectsInBackgroundWithBlock { (results: [PFObject]?, error: NSError) -> Void in {
}

accessing auto-generated fields and querying

I have the following piece of code in ItemController.groovy
def list = {
params.max = 60
def storeYYId = params.id
[itemInstanceList: Item.list(params), itemInstanceTotal: Item.count()]
}
I have the following in Item.groovy:
class Item {
String itemName
static belongsTo = [store:Store]
static constraints = {
itemName(blank:false)
storeId()
}
}
This gives me an error since it tells me that there is no storeId property, but there is, since store_id is a foreign key to the Store table in the corresponding database.
Question1. How do I tell grails to let me access the properties of domains that are autogenerated by GORM, like the id and storeId in this case?
Question2. What code should I write in my ItemController.groovy in my list action, in order to retrieve only a list of items where the storeId == storeYYId ?
Question1. How do I tell grails to let me access the properties of
domains that are autogenerated by GORM, like the id and storeId in
this case?
You should be able to access autogenerated properties in exactly the same way as you access properties that you define. The reason you're getting an error is because Grails does not automatically generate a storeId property for the Item class, the only properties it will autogenerate are version and id (for both Item and Store).
Question2. What code should I write in my ItemController.groovy in my
list action, in order to retrieve only a list of items where the
storeId == storeYYId ?
You'll need to write either a HQL or criteria query to retrieve these items. The criteria query would look something like this (untested)
// Get all items that have storeId = 6
def storeId = 6
def items = Item.withCriteria {
store {
eq('id', storeId)
}
}

Resources