How to store a Vapor Fluent model with decimal field - postgresql

I have a Fluent (Postgres backed) model that requires type Decimal, but I'm only allowed to store .float/.double.
// Model
final class Stat: Model, Content, Equatable {
static let schema: String = "stats"
#ID(key: .id)
var id: UUID?
#Field(key: "name")
var name: String
#Field(key: "earned_run_average")
var era: Decimal // <-- CAN'T DO THIS
}
// Migration
struct CreateStatsTable: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
return database.schema(Stats.schema)
.id()
.field("name", .string, .required)
.field("earned_run_average", .decimal/*.decimal TYPE DOES NOT EXIST */, .required)
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
return database.schema(Stats.schema).delete()
}
}
The above model with migration will fail. If this is not easily achievable (as easy as .string or .double) is there a way to get a behavior similar to type Decimal in Fluent?

Proper Decimal support for Postgres was added recently.
So you should be able to use Decimal in the Model for encoding/decoding to and from a Decimal value in the Postgres DB without any issues.
As for the Migration, there still isn't a .decimal DataType so you can simply use a custom field for the Postgres Database like numeric as so:
.field("earned_run_average", .sql(raw: "NUMERIC(7,2)"), .required)
This should allow you to use Decimal without any issues.

Related

Linking my Fluent model to a pre-existing database table

I've been trying to link up my PostgreSQL database to a Swift Vapor project so I create routes to it. The first table I want to access is a table in my_database calls users. It has the properties user_id (primary integer key) and created_on (timestamp with time zone).
I've linked my Vapor project to my_database so that I can create new models. However, what if I want access to the pre-existing table users. Here's what I've cobbled together from the documentation and a few tutorials:
My initial model:
final class Users: Model {
static let name = "users"
typealias ID = Int
typealias Database = PostgreSQLDatabase
static let idKey: WritableKeyPath<Users, Int?> = \.user_id
var user_id: Int?
var created_on: Date
}
extension Users: Content { }
My migration (which I think is just a 'blank' migration, just to hook the project to the table?):
struct FirstMigration: PostgreSQLMigration {
static func prepare(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {
return conn.future()
}
static func revert(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {
return Future<Void>.done(on: conn)
}
}
And my configuration:
var migrations = MigrationConfig()
migrations.add(migration: FirstMigration.self, database: .psql)
services.register(migrations)
Any guidance much appreciated!
You don't need the migration. You either need to make your model confirm to Migration and add the model as a migration or set the static defaultDatabase property. Then you should be good to go.
PS - I think you want entity not name to tell it what the table name is.

Is there a way to change the Data Type in Realm Database?

As you can see the image, I totally mess up the Data Type (The red circle). Is there a way to change the Data Type into Integer?
EDIT
I want to change the data type from a String to an Int and I have existing data so I can't start with a fresh realm and just change the var type.
class Item: Object {
#objc dynamic var name: String?
#objc dynamic var itemid: String?
#objc dynamic var cateid: String?
}
I may have misunderstand the question but you appear to have existing data stored as as a String and you want to 'convert' all of those to an Int.
You cannot directly change the type to another type and have the stored data changed as well. If you do, it will be flagged with an error.
Error!
Migration is required due to the following errors:
- Property 'Item.itemid' has been changed from 'string' to 'int'.
You need to incorporate a migration block to 'convert' the string value to an Int. Assuming we add a new Int property to our object `item_id', something along these lines will migrate your strings to int's and in the case where the string is not a valid it, it will be assigned a value of 0
Realm.Configuration.defaultConfiguration = Realm.Configuration(
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
if (oldSchemaVersion < 1) {
migration.enumerateObjects(ofType: Item.className()) { oldObject, newObject in
let stringValue = oldObject!["itemid"] as! String
newObject!["item_id"] = Int(stringValue) ?? 0
}
}
})
Also, as soon as Realm is accessed, the object models are written to the Realm file. So a simple matter of
let items = realm.object(Item.self)
Will store that model even if no data was ever written. If, after that line, the var type is changed from a String to an Int, it will throw the migration error.
Deleting the Realm and starting from scratch is one option if that's the case, and as mentioned above, a migration block.
If this is brand new model that has never been used, then as the comments and other answer suggest, just change the String to an Int.
Simply change the String to Int in your Object model. Please note that the Realm documentation says:
String, NSDate, and NSData properties can be declared as optional or non-optional using the standard Swift syntax.
So unlike the String in your previous model, you will not be able to declare your Int as optional. You have two options:
Declare a default value:
class Item: Object {
#objc dynamic var name: String?
#objc dynamic var itemid: Int = 0
#objc dynamic var cateid: Int = 0
}
Declare it as a RealmOptional:
class Item: Object {
#objc dynamic var name: String?
#objc dynamic var itemid = RealmOptional<Int>()
#objc dynamic var cateid = RealmOptional<Int>()
}
For more information on each solution please see this SO answer and the Realm documentation.

Understanding Migrations in Vapor-Fluent (Server side Swift)

I'm writing a web service in Swift using Vapor framework.
I have model named Item. Intially it has only name and id properties.
typealias VaporModel = Content & PostgreSQLModel & Parameter
final class Item: VaporModel {
var id: Int?
var name: String
}
After I configure a controller for the model and add the routes, when I hit the post Item request, I get the error as Model.defaultDatabase is required to use as DatabaseConnectable. I think the error is because I have not added Item to Migrations in configure.swift and I do the same after conforming Item to PostgreSQLMigration.
var migrations = MigrationConfig()
migrations.add(model: Item.self, database: .psql)
services.register(migrations)
Now, I am able to hit the post request and create items in the database.
So I understand that Migration protocol creates the default schema for a model and adds a new table to the database with the model's properties as columns.
Now I want to add a property such as price to my Item class. Now when I hit the post request, I get the error as column "price" of relation "Item" does not exist.
I assume the Migration protocol will be able to identify the schema changes and the column to my table (that's what I was used to in while using Realm for my iOS apps). But I am wrong and I read through the Migration docs and implement the prepare and revert methods in migration like below.
extension Item: PostgreSQLMigration {
static func prepare(on conn: PostgreSQLConnection) -> Future<Void> {
return Database.create(self, on: conn) { creator in
creator.field(for: \.price)
}
}
static func revert(on connection: PostgreSQLConnection) -> EventLoopFuture<Void> {
return Future.map(on: connection) { }
}
}
I'm still struck with the same error column "price" of relation "Item" does not exist. What am I missing here? Is my migration code correct?
Also, I understand that if am not making any changes to the Model, I can comment out the migration config, because they need not run every time I run the service. Is that correct?
With your code you haven't added a new migration. You have implemented a manual initial migration, but the initial migration has run already as requested (migrations.add(model: Item.self, database: .psql). To create a new migration you would need sth like:
struct ItemAddPriceMigration: Migration {
typealias Database = PostgreSQLDatabase
static func prepare(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {
return Database.update(Item.self, on: conn) { builder in
builder.field(for: \.price)
}
}
static func revert(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {
return conn.future()
}
}
And then you need to add it in configure:
migrations.add(migration: ItemAddPriceMigration.self, database: .psql)

Making Realm & Unbox play nice

I am learning to parse JSON in Swift, coming from Android/Java, and I am using Unbox by John Sundell to help me with this, which reminds me of GSON.
Reference: Unbox pod
I use Realm as a database to store data locally.
Reference: Realm.io
It would be great to find a workflow to parse a class with JSON and save it to Realm. I don't want to have a struct that implements Unboxable AND a class that implements Object (Realm), because then I have to reflect the two. That isn't too much work for my current project, but it is kinda ugly...
Did any of you try a similar workflow?
I don't think you need two separate types. My suggestion is to create your objects as Swift classes that inherit from Realm's Object class, and then also conform them to the Unboxable protocol that Unbox offers. (Although the examples on Unbox's page use struct models, there's nothing in the code or documentation that indicates that classes wouldn't work.)
Realm model objects work just like any other classes: in addition to defining whatever properties on the objects you'd like stored in the database, you can also define methods and initializers, and even specify properties that you want Realm to ignore. This allows you to create an object that both serves as a Realm model and also a JSON model compatible with Unbox.
A more concise approach that doesn't require to override required initialisers (based on a tweet by Marin Todorov):
class Car: Object, Unboxable {
dynamic var vendor: String = ""
dynamic var modelName: String = ""
dynamic var electric: Bool = false
required convenience init(unboxer: Unboxer) throws {
self.init()
self.vendor = try unboxer.unbox(key: "vendor")
self.modelName = try unboxer.unbox(key: "modelName")
self.electric = try unboxer.unbox(key: "electric")
}
}
Here is an example that works perfectly for me:
class ProviderRealm: Object, Unboxable {
dynamic var identifier: String = "demo"
dynamic var name: String?
dynamic var logo: String?
/// Initializer used for unboxing of JSON string
required init(unboxer: Unboxer) throws {
self.identifier = (try? unboxer.unbox(key: "identifier")) ?? "demo"
self.name = try? unboxer.unbox(key: "name")
self.logo = try? unboxer.unbox(key: "logo")
super.init()
}
required init(realm: RLMRealm, schema: RLMObjectSchema) {
super.init(realm: realm, schema: schema)
}
required init() {
super.init()
}
required init(value: Any, schema: RLMSchema) {
super.init(value: value, schema: schema)
}
override static func primaryKey() -> String? {
return "identifier"
}
}

How to store optionals

As Realm doesn't support optionals, which are not Object subclasses, I'm trying to wrap a string into StringObject:
final class StringObject: Object {
dynamic var value: String = ""
convenience init?(_ value: String?) {
self.init()
if let value = value {
self.value = value
} else {
return nil
}
}
}
And use it like this:
final class Person: Object {
dynamic var firstName: String = ""
dynamic var lastName: StringObject? // can be optional
}
But this solution has a nasty side effect: as StringOptional values will be stored in their own table within the database, there will be countless duplicate values every time a StringObject is created. I tried making StringObject's value a primary key, but upon Person object's creation I receive an error:
Can't set primary key property 'lastName' to existing value 'Doe'
Which means that internally Realm does not upsert relationships.
Is there a better way to store optionals?
We actually released a beta of Realm that had support for optional strings and data properties, and it will hopefully be released more widely soon! In the meantime, you can try out the beta at https://github.com/realm/realm-cocoa/issues/628#issuecomment-106952727.