Realm, Swift, Many-to-many relationship - ios

On my API I've got a relationship between Stands and Products. In which stands have products, but products can be found in different stands aswell. I'm trying to replicate this relationship on my iOS-application, using Realm but I can't seem to get it working.
The goal of having this relationship is being able to search for Stands that sell particular products.
My model:
class Stand: Object {
dynamic var id : Int = 0
dynamic var name : String = ""
dynamic var latitude : Double = 0.0
dynamic var longitude : Double = 0.0
let products = List<Product>()
override static func primaryKey() -> String? {
return "id"
}
}
class Product: Object {
dynamic var id : Int = 0
dynamic var name : String = ""
let stands = List<Stand>()
override static func primaryKey() -> String? {
return "id"
}
}
When doing my API request for Stands I retrieve the associated Products with it aswell. When I append these to the Stands it works fine for my Stands model as the products are just normally added in the List().
But all of the products are individually created, without having any Stands attached to them.
Is there a way to directly assign these Stands to the Products upon creation of the Products? Just like it's happening the other way around?
My current solution is this..
func retrieveAndCacheStands(clearDatabase clearDatabase: Bool?) {
backend.retrievePath(endpoint.StandsIndex, completion: { (response) -> () in
let listOfProducts : List<(Product)> = List<(Product)>()
func addProducts(stand: Stand, products: List<(Product)>?) {
for product in products! {
print(product.name)
let newProduct = Product()
newProduct.id = product.id
newProduct.name = product.name
newProduct.stands.append(stand)
try! self.realm.write({ () -> Void in
self.realm.create(Product.self, value: newProduct, update: true)
})
}
listOfProducts.removeAll()
}
for (_, value) in response {
let stand = Stand()
stand.id = value["id"].intValue
stand.name = value["name"].string!
stand.latitude = value["latitude"].double!
stand.longitude = value["longitude"].double!
for (_, products) in value["products"] {
let product = Product()
product.id = products["id"].intValue
product.name = products["name"].string!
stand.products.append(product)
listOfProducts.append(product)
}
try! self.realm.write({ () -> Void in
self.realm.create(Stand.self, value: stand, update: true)
})
addProducts(stand, products: listOfProducts)
}
print(Realm.Configuration.defaultConfiguration.path!)
}) { (error) -> () in
print(error)
}
}
This stores the Stands and adds Products to them. It also creates all the products and adds 1 Stand per 10 ish Products (?).
I can't seem to figure out how to make this work. Does anyone else know how to solve this? Or a better solution?

Instead of doing the double-bookkeeping necessary to maintain inverse relationships manually, you should use Realm's inverse relationships mechanism, which provides you with all of the objects pointing to another object using a given property:
class Product: Object {
dynamic var id: Int = 0
dynamic var name: String = ""
// Realm doesn't persist this property because it is of type `LinkingObjects`
// Define "stands" as the inverse relationship to Stand.products
let stands = LinkingObjects(fromType: Stand.self, property: "products")
override static func primaryKey() -> String? {
return "id"
}
}
See Realm's docs on Inverse Relationships for more information.

Related

In realm When ever i am updating it is updating only in 0th index how to solve it?

in realm i given id = 0 and it is as primary key and it will be auto increment, but problem is while updating it is saving in the index path 0 as declare as id : Int = 0.
Where ever i update also it is only updating in 0th index only.
i want to update as per selected object.
What to do?
Program :-
class Discount: Object {
#objc dynamic var id : Int = 0
#objc dynamic var offerName : String = ""
#objc dynamic var percentage: Float = 0.00
#objc dynamic var segmentIndex : Int = 0
#objc dynamic var dateWise: Date?
override class func primaryKey() -> String? {
return "id"
}
//Incrementa ID
func IncrementaID() -> Int{
let realm = try! Realm()
if let retNext = realm.objects(Discount.self).sorted(byKeyPath: "id").last?.id {
return retNext + 1
}else{
return 1
}
}
}
Generally speaking, auto-incrementing primary keys are challenging to deal with and can cause headaches long term.
What's generally most important is ensuring primary keys are unique and using UUID strings is ideally suited for that.
class Discount: Object {
#objc dynamic var discount_id = UUID().uuidString
override static func primaryKey() -> String? {
return "discount_id"
}
}
There may be concern about ordering and often times that managed by either adding a class var to determine ordering; like a timestamp for example or if you want to preserve ordering, objects can be added to a List, which keeps the order, like an array.
To answer your specific question, the code in your question is not complete (it was partially pulled from another question). The reason is that for each object that's created, it must be written to realm first, then the next object's primary key is based on the prior object.
Here's an example.
#objcMembers class User: Object {
dynamic var uid: Int = 0
dynamic var username: String?
func getNextUid() -> Int {
let realm = try! Realm()
if let lastObject = realm.objects(User.self).sorted(byKeyPath: "uid").first {
let lastUid = lastObject.uid
let nextUid = lastUid + 1
return nextUid
}
return 1
}
override static func primaryKey() -> String? {
return "uid"
}
}
now the sequence to use this is as follows
let u0 = User()
u0.uid = u0.getNextUid()
u0.username = "User 0"
let realm = try! Realm()
try! realm.write {
realm.add(u0)
}
let u1 = User()
u1.uid = u1.getNextUid()
u1.username = "User 1"
try! realm.write {
realm.add(u1)
}
as you can see, each object needs to be written to realm in order to the next object to be queried to get the prior objects primary key.
It's a whole lot of potentially unnecessary work and code.
My advice: Stick with the UUID().uuidString for primary keys.

How to query List<Int> on Realm Swift

How can I filter out the RealmFilter.objectIds that has a given Int?
func delete(ids: [Int]) {
let filterResultsToDelete = realm.objects(CRMRealmFilterResult.self).filter("ANY objectIds IN %#",ids)
//Crashes
}
class RealmFilterResult : Object {
#objc dynamic var filterId: Int = 0
let objectIds = List<Int>()
override static func primaryKey() -> String {
return "filterId"
}
}
This may not be at all what you want but it was a good exercise. Maybe this will help.
Let me re-state what I think you're asking: You've got a series of objects that each have a List property of Int's and you want to be able to query for all objects that have a particular int in their list
Using a more real-world example, suppose we have a list of teams and we keep a list of game scores (a list) within each team
class TeamObject: Object {
#objc dynamic var object_id = NSUUID().uuidString
let scoreList = List<ScoreObject>()
override static func primaryKey() -> String? {
return "object_id"
}
}
and we have a score object that stores a score as an Int (and maybe other details like who they played or the date)
class ScoreObject: Object {
#objc dynamic var score = 0
let teamsWithScores = LinkingObjects(fromType: TeamObject.self, property: "scoreList")
}
For simplicity, let's create three scores and two teams and give each team two scores in their list.
let score1 = ScoreObject()
score1.score = 1
let score2 = ScoreObject()
score2.score = 2
let score3 = ScoreObject()
score3.score = 3
let t1 = TeamObject()
t1.scoreList.append(score1)
t1.scoreList.append(score3)
let t2 = TeamObject()
t2.scoreList.append(score2)
t2.scoreList.append(score3)
and write them to realm
try! realm.write {
realm.add(t1)
realm.add(t2)
}
from there, we can get any team that has a score of 1, which solves the question of getting the objects that have a list that contain a given int.
let results = realm.objects(ScoreObject.self).filter("score IN %#", [1])
if results.count > 0 {
for aScore in results {
let teamsWithThisScore = aScore.teamsWithScores
for team in teamsWithThisScore {
print("score: \(aScore.score)")
print(" id: \(team.object_id)")
}
}
} else {
print("no teams with those scores")
}
you can expand on this to get teams (object) that have several scores (ints)
let results = realm.objects(ScoreObject.self).filter("score IN %#", [1,3])
As I said, it may be off base but it does provide a solution in a more object oriented way.
Querying List of primitives (i.e. non Object subclasses, like Int in your case) is not yet supported by Realm.
You can follow the status of this on this GitHub issue.

Swift: Merging 2 or more elements custom objects in array based on unique value

Below is my custom object class.
class UserGroups: NSObject {
let groupName: String
let users: [CheckIn]?
init(json:JSON) {
self.groupName = json[Constants.Models.UserGroups.groupName].stringValue
self.users = UserGroups.getUserGroupsList(jsonArray: json[Constants.Models.UserGroups.users].arrayValue)
}
class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn]{
return jsonArray.flatMap({ (jsonItem: JSON) -> CheckIn in
return CheckIn(json: jsonItem)
})
}
}
I've an array of above custom objects. How can I combine 2 or more custom objects into a single object by merging users of every object having same groupName.
Below is my CheckIn Model:
class CheckIn: NSObject {
let id: String
let firstName: String
let lastName: String
let latitude: String
let longitude: String
let hint: String
init(json: JSON) {
self.id = json[Constants.Models.CheckIn.id].stringValue
self.firstName = json[Constants.Models.CheckIn.firstName].stringValue
self.lastName = json[Constants.Models.CheckIn.lastName].stringValue
self.hint = json[Constants.Models.CheckIn.hint].stringValue
self.latitude = json["location"][Constants.Models.CheckIn.latitude].stringValue
self.longitude = json["location"][Constants.Models.CheckIn.longitude].stringValue
}
}
id field is not unique in CheckIn.
Here's a slightly simplified example that shows how to combine groups that have the same group name.
Here is the UserGroup class. users is now a variable (var) because we will be adding elements to groups to combine them.
class UserGroups: NSObject {
let groupName: String
var users: [String]?
init(groupName: String, users: [String]?) {
self.groupName = groupName
self.users = users
}
}
Here are three groups, two of the share the same group name, Blues.
let group1 = UserGroups(groupName: "Blues", users: ["Tom", "Huck", "Jim"])
let group2 = UserGroups(groupName: "Reds", users: ["Jo", "Ben", "Tommy"])
let group3 = UserGroups(groupName: "Blues", users: ["Polly", "Watson", "Douglas"])
Next, we'll put all the groups in an array.
let allGroups = [group1, group2, group3]
Here, we use Swift's reduce function to allow us to reduce the array to only groups with unique group names.
let compacted = allGroups.reduce([UserGroups](), { partialResult, group in
var dupe = partialResult.filter {$0.groupName == group.groupName }.first
if let dupeGroup = dupe {
dupeGroup.users?.append(contentsOf: group.users ?? [])
return partialResult
} else {
var newPartialResult = partialResult
newPartialResult.append(group)
return newPartialResult
}
})
The array is now reduced to unique groups, we print out all the groups and their users with the help of Swift's map function.
print(compacted.map { $0.users })
// Prints [
Optional(["Tom", "Huck", "Jim", "Polly", "Watson", "Douglas"]),
Optional(["Jo", "Ben", "Tommy"])
]
The Solution
You did not include the CheckIn model, but I will assume that it has some sort of an id field unique to each user. We will use this to make the object Hashable:
// Add this to your file outside of the UserGroups class
extension CheckIn: Hashable {
var hashValue: Int { return self.id }
}
Making it Hashable allows you to convert the Array to a Set, which does not allow duplicates and will remove them in a very efficient way.
// Change getUserGroupsList as follows
class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn] {
return Array(Set(jsonArray.flatMap({ (jsonItem: JSON) -> CheckIn in
return CheckIn(json: jsonItem)
})))
}
Optional Considerations
As an aside, in case you're coming from another language, Swift gives you nice type inference and default names for closure arguments ($0 is the first argument). You can probably make the code a little less verbose, but it's a matter of taste which is preferred.
class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn] {
return Array(Set(jsonArray.flatMap { CheckIn(json: $0) }))
}
Also consider whether you really want the return value to be an array. If you want the list to always have unique users, it is a bit more efficient to use a Set as your return type and forgo the conversion back to an Array like this:
class func getUserGroupsList(jsonArray: [JSON]) -> Set<CheckIn> {
return Set(jsonArray.flatMap { CheckIn(json: $0) })
}
Finally, consider whether you really need the users property to be optional. With sequence types, it is often sufficient to use an empty sequence to denote absence of a value. Depending on your situation, this may simplify your code. The final version looks like this:
class UserGroups: NSObject {
let groupName: String
let users: Set<CheckIn>
init(json:JSON) {
self.groupName = json[Constants.Models.UserGroups.groupName].stringValue
self.users = UserGroups.getUserGroupsList(jsonArray: json[Constants.Models.UserGroups.users].arrayValue)
}
class func getUserGroupsList(jsonArray: [JSON]) -> Set<CheckIn> {
return Set(jsonArray.flatMap { CheckIn(json: $0) })
}
}
Maintaining Order
The caveat is that Set does not maintain the order of the items. If the order of the groups does matter, we can use this solution instead:
class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn] {
var encountered: Set<CheckIn> = []
return jsonArray.flatMap { CheckIn(json: $0) }.filter { encountered.update(with: $0) == nil }
}
In this version, we still use a set, but only to maintain a set of items we've encountered already. The update method on a set returns the same value if it's already in the set or returns nil if it's being inserted for the first time. We use that to filter our array to those items being encountered for the first time while adding them to the set of encountered items to filter them out when they are subsequently encountered again.

Inheritance with Swift Realm, confusion

I have an issue about Inheritance with my Objects in Realm.
Could you please have a look a it. I have :
an Object Activity
an Object Sport which I want to be a subclass of Activity
an Object Seminar which I want to be a subclass of Activity
To make this happen I write, according to the documentation, the following code :
// Base Model
class Activity: Object {
dynamic var id = ""
dynamic var date = NSDate()
override static func primaryKey() -> String? {
return "id"
}
}
// Models composed with Activity
class Nutrition: Object {
dynamic var activity: Activity? = nil
dynamic var quantity = 0
}
class Sport: Object {
dynamic var activity: Activity? = nil
dynamic var quantity = 0
dynamic var duration = 0
}
Now I have an Model Category which I want it to hold the activities, doesn’t matter if it’s an Nutrition or Sport.
Here is my code :
class Categorie: Object {
let activities = List<Activitie>()
dynamic var categoryType: String = ""
override static func primaryKey() -> String? {
return "categoryType"
}
}
Now I try to add a Nutrition object to my List<Activitie> by doing this :
let nutrition = Nutrition(value: [ "activity": [ "date": NSDate(), "id": "0" ], "quantity": 12 ])
try! realm.write {
realm.add(nutrition, update: true)
}
It doesn’t work because List<Activitie> expect an Activity Object and not a Nutrition Object. Where am I wrong ?
Thanks a lot for the help.
You encountered one of the big problems of Realm : there is no complete polymorphism.
This github post gives a big highlight on what is possible or not, and a few possible solutions that you can use.
Quick quote from jpsim from the link above:
Inheritance in Realm at the moment gets you:
Class methods, instance methods and properties on parent classes are
inherited in their child classes.
Methods and functions that take
parent classes as arguments can operate on subclasses.
It does not get you:
Casting between polymorphic classes (subclass->subclass,
subclass->parent, parent->subclass, etc.).
Querying on multiple classes simultaneously.
Multi-class container (RLMArray/List and RLMResults/Results).
According to the article about type erased wrappers in swift and the #5 option I have ended up with something more flexible, here is my solution.
( please note that the solution #5 need to be updated for Swift 3, my solution is updated for Swift 3 )
My main Object Activity
class Activity: Object {
dynamic var id = ""
override static func primaryKey() -> String? {
return "id"
}
}
and my inheritance : Nutrition and Sport
class Nutrition: Activity { }
class Sport: Activity { }
The solution according to the solution #5 option : Using a type-erased wrapper for polymorphic relationships.
If you want to store an instance of any subclass of Activity, define a type-erased wrapper that stores the type's name and the primary key.
class AnyActivity: Object {
dynamic var typeName: String = ""
dynamic var primaryKey: String = ""
// A list of all subclasses that this wrapper can store
static let supportedClasses: [Activity.Type] = [
Nutrition.self,
Sport.self
]
// Construct the type-erased activity from any supported subclass
convenience init(_ activity: Activity) {
self.init()
typeName = String(describing: type(of: activity))
guard let primaryKeyName = type(of: activity).primaryKey() else {
fatalError("`\(typeName)` does not define a primary key")
}
guard let primaryKeyValue = activity.value(forKey: primaryKeyName) as? String else {
fatalError("`\(typeName)`'s primary key `\(primaryKeyName)` is not a `String`")
}
primaryKey = primaryKeyValue
}
// Dictionary to lookup subclass type from its name
static let methodLookup: [String : Activitie.Type] = {
var dict: [String : Activity.Type] = [:]
for method in supportedClasses {
dict[String(describing: method)] = method
}
return dict
}()
// Use to access the *actual* Activitie value, using `as` to upcast
var value: Activitie {
guard let type = AnyActivity.methodLookup[typeName] else {
fatalError("Unknown activity `\(typeName)`")
}
guard let value = try! Realm().object(ofType: type, forPrimaryKey: primaryKey) else {
fatalError("`\(typeName)` with primary key `\(primaryKey)` does not exist")
}
return value
}
}
Now, we can create a type that stores an AnyActivity!
class Category: Object {
var categoryType: String = ""
let activities = List<AnyActivity>()
override static func primaryKey() -> String? {
return "categoryType"
}
}
and to store the data :
let nutrition = Nutrition(value : [ "id" : "a_primary_value"] )
let category = Category(value: ["categoryType" : "0"])
category.activities.append(AnyActivity(tree))
To read the data we want to check the activity method, use the value property on AnyActivity
for activity in activities {
if let nutrition = activity.value as? Nutrition {
// cool it's a nutrition
} else if let sport = activity.value as? Sport {
// cool it's a Sport
} else {
fatalError("Unknown payment method")
}
}
Owen is correct, in regarding OO principles, and I noticed that also, that you are not truly doing inheritance.
When an object uses another as an attribute or property, it is Association, not inheritance. I too am reviewing whether Realm supports Table/Object level inheritance like Java does with Hibernate ... but not expecting it.
This framework while still young but powerful, is good enough for me to avoid using SQLite ... very fast, easy to use and much easier with data model migrations !
In your code Nutrition and Sport don't inherit Activity, they inherit Object and composite an Activity instance. To inherit Activity, you should do
class Nutrition: Activity {
dynamic var quantity = 0
}
I think maybe you worried if you did above, your code did not inherit Object any more. But it is not true. Nutrition is still inherited from Object as Object is inherited by Activity.
Their relations are Nutrition: Activity: Object.
As Yoam Farges pointed it out, Realm doesn't support :
Casting between polymorphic classes (subclass->subclass,
subclass->parent, parent->subclass, etc.).
Which is what I was trying to do.
You guys are right when saying that my Inheritance is not an Inheritance, but as you can see in the Realm documentation it's how you achieve it.
Thanks to the informations I got in this github post I could achieved what I wanted and could keep a easy readability.
Use an option type for polymorphic relationships :
class PolyActivity: Object {
dynamic var nutrition: Nutrition? = nil
dynamic var sport: Sport? = nil
dynamic var id = ""
override static func primaryKey() -> String? {
return "id"
}
}
Create my main Object Activity
class Activity: Object {
dynamic var date = NSDate()
}
and have my Nutrition and Sport object inherited properly to Activity
class Nutrition: Activity {
dynamic var quantity = 0
}
class Sport: Activity {
dynamic var quantity = 0
dynamic var duration = 0
}
My Category object can now hold a List
class Categorie: Object {
let activities = List<PolyActivity>()
var categoryType: String = ""
override static func primaryKey() -> String? {
return "categoryType"
}
}
And this is how I create my Nutrition object :
let polyActivity = PolyActivity(value : [ "id": primaryKey ] )
poly.nutrition = Nutrition(value: [ "date": NSDate(), "quantity": 0, "duration": 0 ])
let category = Category(value: ["categoryType" : "0"])
category.activities.append(polyActivity)
And to retrieve just use Optional Binding :
if let nutrition = category.activities[0].nutrition { }
If you guys have a better, clearer, easier solution please go head !

Query Realm get object which have particularly object in a list

I think this is a simple solution, but i´m stuck with the best approach.
My Realm Database is made with the objects POI and CATEGORY. Where a POI can have one or multiple object of CATEGORY
class POI: Object {
dynamic var id:String = ""
dynamic var name:String = ""
dynamic var visited:Bool = false;
let categories = List<CATEGORY>()
override static func primaryKey() -> String?
{
return "id";
}
}
Later I need to show the number of POI that have a particular CATEGORY, and the number of POI that have a given CATEGORY with the boolean visited has true.
Something like this:
func getAllVisitedPointsWithCategory(idCategory:String) -> Results<POI> {
}
func getAllPointsWithCategory(idCategory:String) -> Results<POI>{
}
Any suggestion?
This should work:
func getAllVisitedPointsWithCategory(idCategory:String) -> [POI] {
let containingPOI = getAllPointsWithCategory(idCategory)
return containingPOI.filter({ (poi) -> Bool in
return poi.visited
})
}
func getAllPointsWithCategory(idCategory:String) -> [POI] {
let realm = try! Realm()
let containingPOI = realm.objects(POI).filter({ (poi) -> Bool in
return poi.categories.contains({ (cat) -> Bool in
return idCategory == cat.id
})
})
return containingPOI
}
It's basically just querying data and filtering the result.
This can be done more concisely and faster by using Realm's query engine rather than filtering the objects individually in Swift:
func getAllVisitedPointsWithCategory(idCategory:String) -> Results<POI> {
return getAllPointsWithCategory(idCategory).filter("visited = true")
}
func getAllPointsWithCategory(idCategory:String) -> Results<POI>{
let realm = try! Realm()
return realm.objects(POI.self).filter("ANY categories.id = %#", idCategory)
}

Resources