How to create many-to-many association using GRDB in Swift? - ios

I'm trying to setup an association between songs and albums. Each song can appear on one or more albums and each album can contain one or more songs. I decided to go with GRDB for my database solution but I'm stuck on this issue.
What I tried:
As documentation suggests, I created a passport struct, like this:
public struct AlbumPassport: TableRecord {
static let track = belongsTo(SPTTrack.self)
static let album = belongsTo(SPTAlbum.self)
}
Then in SPTTrack class:
public static let passports = hasMany(AlbumPassport.self)
public static let albums = hasMany(SPTAlbum.self, through: passports, using: AlbumPassport.album)
And in SPTAlbum class:
public static let passports = hasMany(AlbumPassport.self)
public static let tracks = hasMany(SPTTrack.self, through: passports, using: AlbumPassport.track)
I cannot find in the documentation a good example on how to build a request using those associations. In SPTAlbum class I added linkedTracks property
public var linkedTracks: QueryInterfaceRequest<SPTTrack> {
request(for: Self.tracks)
}
And then in my database manager:
func fetchTracks(for album: SPTAlbum) -> [SPTTrack] {
do {
return try dbQueue.read { db in
try album.linkedTracks.fetchAll(db)
}
} catch {
print(error)
}
return []
}
I'm getting error:
SQLite error 1: no such table: albumPassport
which is pretty self-explanatory, but I have no clue how and where should I create table for the AlbumPassport struct and if there are any additional steps I should take to actually populate this table with album/track connections.
Both SPTTrack/SPTAlbum have a field called id which is set as primaryKey during first migration.

There are no issues with your associations. All hasMany() and belongsTo() are correct. The error you are getting tells me that there is something wrong with your database setup (which you didn't include in your question).
Here is how i would implement it:
import GRDB
struct Album: Codable, Hashable, FetchableRecord, MutablePersistableRecord {
mutating func didInsert(with rowID: Int64, for column: String?) { id = rowID }
var id: Int64?
var name: String
static let passports = hasMany(AlbumPassport.self)
static let tracks = hasMany(Track.self, through: passports, using: AlbumPassport.track)
}
struct Track: Codable, Hashable, FetchableRecord, MutablePersistableRecord {
mutating func didInsert(with rowID: Int64, for column: String?) { id = rowID }
var id: Int64?
var name: String
static let passports = hasMany(AlbumPassport.self)
static let albums = hasMany(Album.self, through: passports, using: AlbumPassport.album)
}
struct AlbumPassport: Codable, Hashable, FetchableRecord, PersistableRecord {
let track: Int64
let album: Int64
static let track = belongsTo(Track.self)
static let album = belongsTo(Album.self)
}
let queue = DatabaseQueue()
try queue.write { db in
try db.create(table: "album") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text).notNull()
}
try db.create(table: "track") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text).notNull()
}
try db.create(table: "albumPassport") { t in
t.column("track", .integer).notNull().indexed().references("track")
t.column("album", .integer).notNull().indexed().references("album")
t.primaryKey(["track", "album"])
}
// Testing real data from https://music.apple.com/de/artist/yiruma/73406786
var solo = Album(name: "SOLO")
try solo.insert(db)
var sometimes = Track(name: "Sometimes Someone")
try sometimes.insert(db)
try AlbumPassport(track: sometimes.id!, album: solo.id!).insert(db)
var destiny = Track(name: "Destiny Of Love")
try destiny.insert(db)
try AlbumPassport(track: destiny.id!, album: solo.id!).insert(db)
var bestOf = Album(name: "Best of Yiroma")
try bestOf.insert(db)
var poem = Track(name: "Poem")
try poem.insert(db)
try AlbumPassport(track: poem.id!, album: bestOf.id!).insert(db)
var river = Track(name: "River Flows In You")
try river.insert(db)
try AlbumPassport(track: river.id!, album: bestOf.id!).insert(db)
}
// Fetch all albums and their tracks
try queue.read { db in
struct AlbumInfo: FetchableRecord, Decodable, CustomStringConvertible {
var album: Album
var tracks: Set<Track>
var description: String { "\(album.name) → \(tracks.map(\.name))" }
}
let request = Album.including(all: Album.tracks)
let result = try AlbumInfo.fetchAll(db, request)
print(result)
// > [SOLO → ["Sometimes Someone", "Destiny Of Love"], Best of Yiroma → ["River Flows In You", "Poem"]]
}
See my answer of Many-to-many relationship where one entity has multiple properties of the same type for another code example.

Related

Query relation images from Parse that uses Back4App database

So I have an ParseObject setup as this - This is the main Object in Parse called MGLocation:
struct MGLocation: ParseObject {
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var originalData: Data?
var ACL: ParseACL?
var title: String?
var category: String?
init() {}
init(objectId: String?) {
self.objectId = objectId
}
}
Then I have my Codable setup using the following code:
struct Place: Codable, Identifiable {
let id: Int
var b4aId = ""
let title: String?
let category: String
init(
id: Int,
title: String?,
category: String,
) {
self.id = id
self.title = title
self.category = category
}
init(with p: MGLocation) {
self.id = atomicId.wrappingIncrementThenLoad(ordering: .relaxed)
self.b4aId = p.objectId ?? ""
self.title = p.title ?? "Undefined"
self.category = p.category ?? "Uncategorized"
}
}
Then I have the following function which pulls in the MGLocation:
func fetchPlaces() {
let query = MGLocation.query().limit(1000)
query.find { [weak self] result in
guard let self = self else {
return
}
switch result {
case .success(let items):
self.places = items.map({
Place(with: $0)
})
case .failure(let error):
}
}
}
Questions:
What is the best way that I can pull in a relational column? Inside MGLocation, I have a related column called images which can access another object.
This calls MGImage which has the following columns:
id, title, column, mime
Does anyone know how I can infuse and pull in the related column also? All help will be appreciated!
I recommend using Parse-Swift from here: https://github.com/netreconlab/Parse-Swift, as oppose to the parse-community one. I'm one of the original developers of Pase-Swift (you can see this on the contributors list) and I don't support the parse-community version anymore. The netreconlab version is way ahead of the other in terms of bug fixes and features and you will get better and faster support from the netreconlab since I've been the one to address the majority of issues in the past.
What is the best way that I can pull in a relational column? Inside MGLocation, I have a related column called images which can access another object.
I always recommend looking at the Playgrounds for similar examples. The Playgrounds has Roles and Relation examples. Assuming you are using version 4.16.2 on netreconlab. The only way you can do this with your current relation on your server is like so:
// You need to add your struct for MGImage on your client
struct MGImage: ParseObject { ... }
struct MGLocation: ParseObject {
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var originalData: Data?
var ACL: ParseACL?
var title: String?
var category: String?
var images: ParseRelation<Self>? // Add this relation
/* These aren't needed as they are already given for free.
init() {}
init(objectId: String?) {
self.objectId = objectId
}
*/
}
//: It's recommended to place custom initializers in an extension
//: to preserve the memberwise initializer.
extension MGLocation {
// The two inits in your code aren't needed because ParseObject gives them to you
}
//: Now we will see how to use the stored `ParseRelation on` property in MGLocation to create query
//: all of the relations to `scores`.
Task {
do {
//: Fetch the updated location since the previous relations were created on the server.
let location = try await MGLocation(objectId: "myLocationObjectId").fetch()
print("Updated current location with relation: \(location)")
let usableStoredRelation = try location.relation(location.images, key: "images")
let images = try await (usableStoredRelation.query() as Query<MGImage>).find()
print("Found related images from stored ParseRelation: \(images)")
} catch {
print("\(error.localizedDescription)")
}
}
If your images column was of type [MGImage] instead of Relation<MGImage> then you would be able to use var images: [MGImage]? in your MGLocation model and simply use include on your query. You can see more here: https://github.com/netreconlab/Parse-Swift/blob/325196929fed80ca3120956f2545cf2ed980616b/ParseSwift.playground/Pages/8%20-%20Pointers.xcplaygroundpage/Contents.swift#L168-L173

Two different model types in same view model? Swift - Xcode?

Im working on a project which uses GraphQL to get product information from a shopify store.
I query data by searching the products ProductID and get the data in the form Storefront.Product.
However my view model requires it to be in the form Storefront.ProductEdge.
Im trying to adding two different model types (Storefront.Product and Storefront.ProductEdge) in the same view model (ProductViewModel).
Heres my code:
import Foundation
import Buy
final class ProductViewModel: ViewModel {
typealias ModelType = Storefront.ProductEdge
typealias ModelType1 = Storefront.Product
let model: ModelType
let model1: ModelType1
let cursor: String
var id: String
var title: String
var summary: String
var price: String
var images: PageableArray<ImageViewModel>
var variants: PageableArray<VariantViewModel> ///Ive changed let to var here so i can assign values in HomeController
// ----------------------------------
// MARK: - Init -
//
required init(from model: ModelType) {
self.model = model
self.cursor = model.cursor
let variants = model.node.variants.edges.viewModels.sorted {
$0.price < $1.price
}
let lowestPrice = variants.first?.price
self.id = model.node.id.rawValue
self.title = model.node.title
self.summary = model.node.descriptionHtml
self.price = lowestPrice == nil ? "No price" : Currency.stringFrom(lowestPrice!)
self.images = PageableArray(
with: model.node.images.edges,
pageInfo: model.node.images.pageInfo
)
self.variants = PageableArray(
with: model.node.variants.edges,
pageInfo: model.node.variants.pageInfo
)
}
required init(from model1: ModelType1){
self.model1 = model1
self.cursor = ""
let variants = model1.variants.edges.viewModels.sorted {
$0.price < $1.price
}
let lowestPrice = model1.variants.edges.first?.viewModel.price
self.id = model1.id.rawValue
self.title = model1.title
self.summary = model1.descriptionHtml
self.price = lowestPrice == nil ? "No price" : Currency.stringFrom(lowestPrice!)
self.images = PageableArray(
with: model1.images.edges,
pageInfo: model1.images.pageInfo
)
self.variants = PageableArray(
with: model1.variants.edges,
pageInfo: model1.variants.pageInfo
)
}
}
extension Storefront.ProductEdge: ViewModeling {
typealias ViewModelType = ProductViewModel
}
It is saying
Return from initializer without initializing all stored properties
Is this possible to do?
please help?
EDIT:
By making model and model1 options or using Enums on model or model1, I get the error
Type 'ProductViewModel' does not conform to protocol 'ViewModel'
Do I need to somehow do an If statement or enum on Storefront.ProductEdge and Storefront.Product??
here is the ViewModel protocol:
import Foundation
protocol ViewModel: Serializable {
associatedtype ModelType: Serializable
var model: ModelType { get }
init(from model: ModelType)
}
extension ViewModel {
static func deserialize(from representation: SerializedRepresentation) -> Self? {
if let model = ModelType.deserialize(from: representation) {
return Self.init(from: model)
}
return nil
}
func serialize() -> SerializedRepresentation {
return self.model.serialize()
}
}
. I am unable to change this protocol.
Please help?
In Swift you are required to assign a value to all properties before exiting an initializer. It appears that in both cases you're either not assigning a value to model or model1. If it's intentional for on our the other to be used, but not both, I'd recommend an enum with an associated value. Something like this:
enum ModelObject {
case model(Model)
case model1(Model1)
}
Then in your initializer you can do:
self.modelObject = .model1(model1)
or
self.modelObject = .model(model)
If for some reason that doesn't work, you could also make them both optional and assign one to nil and the other to the value it was initialized with.

How to avoid adding the same data model that has the same primary key in realm database?

I have one to many relationship between two models, Product and WishList like the code below
class Product : Object {
#objc dynamic var productID : String = ""
#objc dynamic var name : String = ""
#objc dynamic var unitPrice: Double = 0.0
#objc dynamic var imagePath : String = ""
#objc dynamic var quantity = 0
#objc dynamic var hasBeenAddedToWishList : Bool = false
var parentCategory = LinkingObjects(fromType: WishList.self, property: "products")
convenience init(productID : String, name: String, unitPrice: Double, imagePath: String, quantity: Int = 1, hasBeenAddedToWishList: Bool = false) {
self.init()
self.productID = productID
self.name = name
self.unitPrice = unitPrice
self.imagePath = imagePath
self.quantity = quantity
self.hasBeenAddedToWishList = hasBeenAddedToWishList
}
override static func primaryKey() -> String? {
return "productID"
}
}
and WishList:
class WishList : Object {
#objc dynamic var userID: String = ""
var products = List<Product>()
}
I try to add or remove product to WishList using the code below when love button in the image above is pressed :
// 1. get the wishlist based on UserID
let allWishList = realm.objects(WishList.self)
let theWishList = allWishList.filter("userID CONTAINS[cd] %#", userID).first
guard let userWishList = theWishList else {return}
// 2. modify Wishlist data in Realm.
if loveIconHasBeenFilled {
guard let index = userWishList.products.index(where: {$0.productID == selectedProduct.productID}) else {return}
do {
// remove data from realm database
try realm.write {
userWishList.products.remove(at: index)
}
} catch {
// error Handling
}
} else {
do {
// add product to wishlist model in realm database
try realm.write {
userWishList.products.append(selectedProduct)
}
} catch {
// error Handling
}
}
and here is the data in Realm Browser
and the problem is ....
when I run the app for the first time, I can add, and then remove, and then add the product again to the wishlist, and the number of product in the realm database still be the same (all have unique productID)
but when I restart the app, and try to click that love button to add the product to wishlist again, it throws an error
'RLMException', reason: 'Attempting to create an object of type
'Product' with an existing primary key value 'a'
this error is triggered because of this line of code userWishList.products.append(selectedProduct) , when adding the product to WishList, it automatically adds Product in the realm database. so because I keep adding the same product that has the same productID (primary key) it will throw that error.
so, my question is, how to avoid addition in Product if it has the same productID (primary key), it is better if i can just update the product in realm database when adding the product to the wishlist using this line of code: userWishList.products.append(selectedProduct)
You could check the property hasBeenAddedToWishList of the selected product and only add it if the property is false.
if loveIconHasBeenFilled {
//your logic to remove already added products
} else if !selectedProduct.hasBeenAddedToWishList { //<--- check if the product already exists in wishlist if not you add it
do {
// add product to wishlist model in realm database
try realm.write {
userWishList.products.append(selectedProduct)
}
} catch {
// error Handling
}
}

How to connect items to category in one to many relationship when insert data

i have 2 entities one customers and other is cases, the relationship is one to many one customer had many cases and the relation names toCustomers for cases entity and toCases for customers entity so in my code i pass selected customer name by segue to the add cases UIViewController
var CustmerNameForAddNewCaseVar: Customers?
CustomerNameLbl.text = CustmerNameForAddNewCaseVar?.name
in the save button
let newCase : Cases!
newCase = Cases(context:context)
newCase.caseName = caseNameTextField.text
newCase.toCustomers = CustmerNameForAddNewCaseVar?.name // here i got error ... Cannot assign value of type 'String?' to type 'Customers?'
do{
AppDel.saveContext()
}catch{
print(error)
}
any help
thank you
To use your example... you would alter your code to match this:
let newCase : Cases!
newCase = Cases(context:context)
newCase.caseName = caseNameTextField.text
newCase.toCustomers = CustmerNameForAddNewCaseVar
do {
// Save Context
} catch {
// Handle Error
}
Notice the line "newCase.toCustomers" has changed.
What others are suggesting in the comments to your question is to clear up the naming of your entities and variables to help clarify usage. An example would be:
import UIKit
import CoreData
class Customer: NSManagedObject {
#NSManaged var cases: Set<Case>?
}
class Case: NSManagedObject {
#NSManaged var customer: Customer?
#NSManaged var name: String?
}
class ViewController: UIViewController {
var customer: Customer?
var context: NSManagedObjectContext?
#IBOutlet weak var caseName: UITextField?
#IBAction func didTapSave(_ sender: UIButton) {
guard let context = self.context else {
return
}
guard let customer = self.customer else {
return
}
let newCase = Case(context: context)
newCase.name = caseName?.text
newCase.customer = customer
do {
try context.save()
} catch {
// Handle Error
}
}
}

Realm, Swift, Many-to-many relationship

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.

Resources