Native MongoDB driver for Swift, written in Swift. This library does not wrap around the mongoc driver. It uses:
- Hummingbird for TCP connections
- Our own BSON library, which is also 100% native Swift
- A mongoDB server
- The swift version described in
.swift-version
, see swiftenv.
We don't support any other version of swift with the constantly changing syntax. This required swift version changes constantly with newer versions of MongoKitten
and it's recommended to pin down the version in SPM.
Note: other versions of swift
and MongoDB
may or may not work. We do not support them.
The unit tests expect a test database. Run the Tools/testprep.sh script to import it.
Add MongoKitten
to your Package.swift:
import PackageDescription
let package = Package(
name: "MyApp",
dependencies: [
.Package(url: "https://github.com/PlanTeam/MongoKitten.git", majorVersion: 0, minor: 8)
]
)
Or for Swift 04-12 users:
import PackageDescription
let package = Package(
name: "MyApp",
dependencies: [
.Package(url: "https://github.com/PlanTeam/MongoKitten.git", majorVersion: 0, minor: 7)
]
)
Import the MongoKitten library:
import MongoKitten
Connect to your local MongoDB server using an URI:
let server: Server!
do {
server = try Server("mongodb://username:password@localhost:27017", automatically: true)
} catch {
// Unable to connect
print("MongoDB is not available on the given host and port")
}
Select a database to use for your application:
let database = server["mydatabase"]
And select your collections to use from the database:
let userCollection = database["users"]
let otherCollection = database["otherdata"]
In MongoKitten
we use our own BSON
library for working with MongoDB Documents.
You can create a simple user document like this:
var userDocument: Document = [
"username": "Joannis",
"password": "myPassword",
"age": 19,
"male": true
]
If you want to embed a variable you'll need to use the ~
prefix operator.
let niceBoolean = true
let testDocument: Document = [
"example": "data",
"userDocument": ~userDocument,
"niceBoolean": ~niceBoolean,
"embeddedDocument": [
"name": "Henk",
"male": false,
"age": 12,
"pets": ["dog", "dog", "cat", "cat"]
]
]
A Document is similar to a Dictionary. A document however has order and thus the position of elements doesn't change unless you tell it to.
A Document is therefore an array and a dictionary at the same time. With the minor difference that a Document can only hold BSON's Value
. The problem that arises it when you want to use native types from Swift like a String, Int or another Document (sub-document) and elements in there.
We fixed this with the use of subscripts and getters.
To get a value from the Document you can subscript it like this:
let username: Value = userDocument["username"]
Documents always return a value. When the value doesn't exist we'll return Value.nothing
.
If you want to get a specific value from the Document like a String we can return an optional String like this:
let username: String? = userDocument["username"].stringValue
However.. for an age you might want a String without receiving nil
in a case like this:
let age: String? = userDocument["age"].stringValue
We made this easier by converting it for you:
let age: String = userDocument["age"].string
However.. if the age would normally be .nothing
we'll now return an empty string ""
instead. So check for that!
Last but not least we'll also want to assign data using a subscript. Because subscript are prone to being ambiguous we had to use enums for assignment.
This would result in this:
userDocument["bool"] = .boolean(true)
userDocument["int32"] = .int32(10)
userDocument["int64"] = .int64(200)
userDocument["array"] = .array(["one", 2, "three"])
userDocument["binary"] = .binary(subtype: .generic, data: [0x00, 0x01, 0x02, 0x03, 0x04])
userDocument["date"] = .dateTime(NSDate())
userDocument["null"] = .null
userDocument["string"] = .string("hello")
userDocument["objectID"] = .objectId(try! ObjectId("507f1f77bcf86cd799439011"))
Of course variables can still use the ~
operator:
let trueBool = true
userDocument["newBool"] = ~trueBool
Using the above document you can insert the data in the collection.
try userCollection.insert(userDocument)
In the collection's insert method you can also insert a group of Documents: [Document]
try otherCollection.insert([testDocument, testDocument, testDocument])
To find the Documents in the collection we'll want to use find
or findOne
on the collection. This returns a "cursor".
The find
and findOne
functions are used on a collection and don't require any parameters.
Adding parameters, however, helps finding the data you need. By providing no arguments we're selecing all data in the collection.
let resultUsers = try userCollection.find()
This returns a cursor that you can use to loop over users. MongoKitten
's Cursor
by default loads 10 Documents at a time from MongoDB
which is customizable to a bigger or smaller amount of Documents.
This allows us to provide a smaller delay when looping over data. This also allows the application to remove the cursor halfway through the Documents without downloading Documents that aren't being used.
Looping over the above results is easy:
for userDocument in resultUsers {
print(userDocument)
if userDocument["username"].stringValue == "harriebob" {
print(userDocument)
}
}
If you do want all Documents in one array you can use Array()
.
let otherResultUsers = try userCollection.find()
let allUserDocuments = Array(otherResultUsers)
But be careful.. a cursor contains the data only once.
let depletedExample = try userCollection.find()
// Contains data
let allUserDocuments = Array(depletedExample)
// Doesn't contain data
let noUserDocuments = Array(depletedExample)
We also have a query builder which can be easily used to create filters when searching for Documents.
let q: Query = "username" == "Joannis" && "age" > 18
let result = try userCollection.findOne(matching: q)
Or simpler:
let newResult = try userCollection.findOne(matching: "username" == "Joannis" && "age" > 18)
This comes in handy when looping over data:
for user in try userCollection.find(matching: "male" == true) {
print(user["username"].string)
}
Updating data is simple too. There is a multiple
argument for people who update more than one document at a time. This example only updates the first match:
try userCollection.update(matching: ["username": "Joannis"], to: ["username": "Robbert"])
Deleting is possible using a document and a query
// Delete using a document
try userCollection.remove(matching: ["username": "Robbert"])
// Make a GridFS Collection within the database 'mydatabase'
let gridFS = GridFS(in: server["mydatabase"])
// Find all bytes corresponding to this image
let data = NSData(contentsOfFile: "./myimage.jpg")!
// Store the file in GridFS with maximum 10000 bytes per chunk (255000 is the recommended default) and doesn't need to be set
// Store the ObjectID corresponding to the file in a constant variable
let objectID = try! gridFS.store(data: data, named "myimage.jpg", withType: "image/jpeg", inChunksOf: 10000)
// Retreive the file from GridFS
let file = try! gridFS.findOne(byID: objectID)
// Get the bytes we need
let myImageData: [Byte] = file!.read(from: 1024, to: 1234)
Imagine running a video streaming site. One of your users uploads a video. This will be stored in GridFS within 255000-byte chunks.
Now one user starts watching the video. You'll load the video chunk-by-chunk without keeping all of the video's buffer in memory.
The user quits the video about 40% through the video. Let's say chunk 58 of 144 of your video. Now you'll want to start continueing the video where it left off without receving all the unneccesary chunks.
In our library MongoDB servers under version 3.2 can receive malicious queries when using find
.
They can be used to execute DBCommands.
MongoKitten is licensed under the MIT license.