Mapped Types¶
Mapped types are Python types that don’t inherit from mincePy classes and can therefore exist entirely independently. This is extremely useful if, for example, you are using objects from someone else’s that you can’t change or you choose not to change because it is also used independently of a database.
Using type helpers¶
Let’s demonstrate with a Car object
[1]:
class Car:
def __init__(self, make='ferrari', colour='red'):
self.make = make
self.colour = colour
def __str__(self):
return "{} {}".format(self.colour, self.make)
So far, mincePy can’t do anything with this:
[2]:
import mincepy
historian = mincepy.connect('mongodb://127.0.0.1/mince-mapped-types', use_globally=True)
ferrari = Car()
historian.save(ferrari)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-255c629cc686> in <module>
3
4 ferrari = Car()
----> 5 historian.save(ferrari)
6
~/src/mincepy/mincepy/historians.py in save(self, *objs)
187 with self.in_transaction():
188 for entry in to_save:
--> 189 ids.append(self.save_one(*entry))
190
191 if len(objs) == 1:
~/src/mincepy/mincepy/historians.py in save_one(self, obj, meta)
211 # Save the object and metadata
212 with self.in_transaction():
--> 213 record = self._save_object(obj, self._live_depositor)
214 if meta:
215 self.meta.update(record.obj_id, meta)
~/src/mincepy/mincepy/historians.py in _save_object(self, obj, depositor)
783 helper = self._ensure_compatible(type(obj))
784 except TypeError:
--> 785 raise TypeError(
786 "Type is incompatible with the historian: {}".format(type(obj).__name__)) from None
787
TypeError: Type is incompatible with the historian: Car
To tell mincePy about Car
s we need to define subclass of TypeHelper which helps mincePy to understand your type…understandably…
[3]:
import uuid
class CarHelper(mincepy.TypeHelper):
TYPE = Car
TYPE_ID = uuid.UUID('21605412-30e5-4f48-9f56-f0fa8014e746')
make = mincepy.field()
colour = mincepy.field()
historian.register_type(CarHelper)
ferrari_id = historian.save(ferrari)
print(ferrari_id)
5f75cf4dc5e3bf28a7a85d9c
…and that’s it! MincePy can now work with Car
s. You’ll notice that, unlike many ORMs, we haven’t specified the types of make
and colour
, nor any validation options like the maximum length of the strings or whether they can be missing or not. This is deliberate. MincePy leaves validation up to your code (so you do whatever you would have done if there was no database involved) and concerns itself with getting your object in and out of the database. Speaking of which, let’s see
some of that in action.
[4]:
del ferrari
loaded_ferrari = historian.load(ferrari_id)
print(loaded_ferrari)
red ferrari
Cool, so how does that work? Well mincePy has created a DataRecord of our Car
in the database that stores a bunch of things, including the state which can be used to recreate it. Let’s have a look:
[5]:
print(historian.records.get(ferrari_id))
obj_id 5f75cf4dc5e3bf28a7a85d9c
type_id 21605412-30e5-4f48-9f56-f0fa8014e746
creation_time 2020-10-01 14:45:01.673000
version 0
state {'make': 'ferrari', 'colour': 'red'}
state_types [[[], UUID('21605412-30e5-4f48-9f56-f0fa8014e746')]]
snapshot_hash 17480f325c8a48d9a5ea1163fcda3ff3cf0940deff21e7df6c7a72b5b626bf69
snapshot_time 2020-10-01 14:45:01.673000
extras {'_user': 'martin', '_hostname': 'deca'}
In addition to the state we see the creation and snapshots times, the version number and other information mincePy needs to store and track the object.
Let’s create some more Car
s and perform some queries.
[6]:
for make in 'skoda', 'honda', 'bmw':
for colour in 'red', 'green', 'violet':
historian.save(Car(make=make, colour=colour))
historian.find().count()
[6]:
10
We can, for example, find all the red ones using:
[7]:
results = historian.find(CarHelper.colour == 'red')
for entry in results:
print(entry)
red ferrari
red skoda
red honda
red bmw
References¶
The next thing we may want to introduce is references. What if we have an object like this:
[8]:
class Person:
def __init__(self, name, car=None):
self.name = name
self.car = car
def __str__(self):
return self.name if self.car is None else self.name + "({})".format(self.car)
matt = Person('matt', loaded_ferrari)
Here we want Person
objects to be able to store a reference (a foreign key in ORM language) to the Car
that they own. No problem, let’s define a new helper:
[9]:
class PersonHelper(mincepy.TypeHelper):
TYPE = Person
TYPE_ID = uuid.UUID('80c7bedb-9e51-48cd-afa9-04ec97b20569')
name = mincepy.field()
car = mincepy.field(ref=True)
historian.register_type(PersonHelper)
matt_id = historian.save(matt)
By using setting ref=True
we tell mincePy that we want to the car
field to be stored by reference rather than keeping a copy of the car in the record. Let’s have a look:
[10]:
print(historian.records.get(matt_id))
obj_id 5f75cf51c5e3bf28a7a85da6
type_id 80c7bedb-9e51-48cd-afa9-04ec97b20569
creation_time 2020-10-01 14:45:05.364000
version 0
state {'name': 'matt', 'car': {'obj_id': ObjectId('5f75cf4dc5e3bf28a7a85d9c'), 'version': 0}}
state_types [[[], UUID('80c7bedb-9e51-48cd-afa9-04ec97b20569')], [['car'], UUID('633c7035-64fe-4d87-a91e-3b7abd8a6a28')]]
snapshot_hash 963c248f43a2cc8ff187c18e23b815f1f40df5a89ca2858346150cb6d0226a0a
snapshot_time 2020-10-01 14:45:05.364000
extras {'_user': 'martin', '_hostname': 'deca'}
We see that the car
field in the state
dictionary is in fact a reference pointing to the object id of the Ferrari. What does this all mean in practice? Well let’s see what happens when we load the matt
object from the database:
[11]:
del matt
loaded_matt = historian.load(matt_id)
loaded_matt.car is loaded_ferrari
[11]:
True
If we add another Person
referring to the Ferrari we see that they share a reference to the same instance, as expected.
[12]:
rob = Person('rob', loaded_ferrari)
rob_id = historian.save(rob)
del rob, loaded_matt
loaded_ferrari.colour = 'yellow'
historian.save(loaded_ferrari)
del loaded_ferrari
matt = historian.get(matt_id)
rob = historian.get(rob_id)
print(matt.car is rob.car)
print(matt.car)
True
yellow ferrari
So, that gets you up to speed on the basics of using mapped types in mincePy. Have a look at the API reference and post an issue here if there is anything else you would like to see documented.