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 Cars 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 Cars. 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 Cars 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.