Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SQLite persistent normalized cache #79

Merged
merged 76 commits into from
Apr 24, 2017

Conversation

kompfner
Copy link
Contributor

@kompfner kompfner commented Apr 11, 2017

This PR does the following:

  • Introduces SqliteNormalizedCache, which persists records in a SQLite database.
  • Introduces the SqliteNormalizedCache subspec, which is necessary for two reasons:
    • It allows consumers of the Apollo pod to decide whether or not they need SQLite persistence for their application - if they don’t need it, they don’t need to take a dependency on SQLite.swift.
    • It disables SqliteNormalizedCache usage on tvOS projects, which don’t have access to the file system.
  • Shares existing unit tests to test InMemoryNormalizedCache and SqliteNormalizedCache.
  • Adds a unit test verifying that cached data is persisted in SqliteNormalizedCache.

Known remaining work before this PR can be merged:

  • Properly handle versioning both Apollo.xcodeproj and ApolloSQLite.xcodeproj
  • Naming: "Sql" --> "SQL"
  • In loadRecords(forKeys:), implement correct expected behavior of returning nil for keys that don't have corresponding records (instead of the misunderstood expected behavior described in the comment)

Next steps:

  • Clear cached records when Apollo version changes.

@apollo-cla
Copy link

@kompfner: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Meteor Contributor Agreement here: https://contribute.meteor.com/

Paul Kompfner added 28 commits April 11, 2017 17:22
…framework directory by the time StarWarsAPI generate API build phase is run
…l] instead of []. This is an odd behavior, but is what is currently required by ApolloStore. This should probably be revisited.
…ached by the time the fetch’s completion block was called
@justinmakaila
Copy link
Contributor

But SQLite isn't lightweight. It requires a new dependency, new xcodeproj, and adds more overhead if you're not a CocoaPods user. Lunar doesn't. It literally just links CoreData. Not to mention it pretty much is this SQLiteNormalizedCache, just with DB transactions expressed in CoreData logic, not SQLite.

If you want to use Apollo and Lunar, you add this to your Cartfile:

github "justinmakaila/Lunar"

It will clone Lunar, clone Apollo, and compile both to .frameworks for you. You then can link both of them as necessary. Sure, when you initialize your ApolloStore, you have to pass it a LunarCache, but that's not even painful. It literally removes the black magic of Apollo just using some "Magic Cache" that you don't have access to.

(Side note: I initially brought this up and started development on Lunar because I wanted to use our already existing model framework. After exploring the code shared here, I realized that I don't need that dependency anymore, because Lunar now has a single entity, LunarRecord, which provides two properties: "key" and "record".)

I'm not happy, as a contributor, a maintainer of my own cache (that literally does the same thing without the dependency), and more specifically, as a Carthage user, that we're trending towards including something because it was PR'd first and is easy to maintain if you use CocoaPods.

My dependency build time is going to go up because I now have to build Apollo, Lunar, and SQLite (which I don't use), even though I cut my local dependencies out (no more model or sync frameworks).

I share the general sentiment here that we should be able to get people up and running with persistence quickly, and that's why I'm still proposing that we just provide install instructions with how to include whatever specific community-maintained cache. It's really not beyond our audience to have to make some choices on their own backend and maybe, if there's no community support, help support the community and write some code.

@justinmakaila
Copy link
Contributor

justinmakaila commented Apr 19, 2017

Apollo's beauty isn't even in it's caching, nor should it concern itself with persistence. Apollo's beauty is that it was a lightweight interface that could live on top of your network and persistence layer without being opinionated about either.

This takes a step away from that, and starts to add weight and overhead, absolutely.

@justinmakaila
Copy link
Contributor

justinmakaila commented Apr 19, 2017

At this point, I've said all there is to say. If you guys are set on merging this in and forcing SQLite on Carthage users, than there's nothing I can do. I'm just trying to share what I've learned in working with Moya and what I'm trying to use Apollo for. SQLite has nothing to do with Apollo, IMO, and should not be included in the repository.

…ping

JSONSerializationFormat is meant to convert from/to GraphQL types, but
the fields in a record already contain JSON values or Reference.
@martijnwalraven
Copy link
Contributor

@kompfner: I cleaned up some code related to promises and the fields serialization. We no longer need to special case NS types now. I tried to clarify typing by adding some typealiases to Record, but I’m not sure if this actually makes things more or less readable. Definitely something we should also document. Anyway, the rest of the code looks pretty simple, and all tests seem to pass, so I think we’re ready to merge (squash)!

reject(error)
}
}
return Promise { try mergeRecords(records: records) }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, sweet, your Promise implementation automatically turns thrown errors into rejections. 👍

@@ -117,14 +117,13 @@ extension Dictionary: JSONEncodable {
public var jsonObject: JSONObject {
var jsonObject = JSONObject(minimumCapacity: count)
for (key, value) in self {
print("key: \(key), \(type(of: key))")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be removed?

let recordJSON = try SQLiteSerialization.deserialize(data: recordData)
return Record(key: row[key], recordJSON)
let fields = try SQLiteSerialization.deserialize(data: recordData)
return Record(key: row[key], fields)
}
}

private let serializedReferenceKey = "reference"

final class SQLiteSerialization {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I think this should probably be private.

}

private static func deserialize(valueJSON: Any) throws -> Any {
switch valueJSON {
private static func deserialize(jsonValue: JSONValue) throws -> Record.Value {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this for the naming of the serialize and deserialize method parameters, for even greater clarity?

  • serialize(fieldValue:)
  • serialize(fieldJSON:)

var objectToSerialize = JSONObject()
for (key, value) in fields {
objectToSerialize[key] = try serialize(value: value)
}
return try JSONSerializationFormat.serialize(value: objectToSerialize)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup in this commit is great! Perhaps JSONSerializationFormat should have a more descriptive name, to make its purpose more distinct than the system-provided JSONSerialization?

@@ -88,7 +88,7 @@ public final class SQLiteNormalizedCache: NormalizedCache {
}
}

private let serializedReferenceKey = "reference"
private let serializedReferenceKey = "$reference"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really necessary? What distinguishes a reference field from any other field is that it's a dictionary (of the form [reference: <key>]), whereas other fields are primitive types.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right, I wasn't thinking clearly, there are no nested objects in records of course. Although we may want to leave open the possibility of storing JSON in a field if something like that becomes part of the GraphQL spec? May be too forward looking.

@kompfner
Copy link
Contributor Author

@martijnwalraven I'm going to do a tiny bit more cleanup based on the comments I left on your last few commits. Please let me know if you agree with those. Those commits were really nice improvements, btw!

@kompfner
Copy link
Contributor Author

kompfner commented Apr 21, 2017

I think the reason the ApolloSQLite unit tests were failing was due to race conditions around multiple tests attempting to open the same file on disk. My fix was to have each test open a SQLite file at a different file URL.

Paul Kompfner added 2 commits April 21, 2017 12:53
@kompfner
Copy link
Contributor Author

kompfner commented Apr 21, 2017

Disabling iOS 9.3 unit tests, since that simulator doesn't seem to be an available option on Travis right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants