I have several very similar subclasses that I (think I) want to be stored in the same database table. Most of the fields are identical, with each subclass adding 1 or 2 custom fields. My code is using the Django ORM, but I think the question is more general than that.Problem specificationThe app is meant to simplify the tracking and submission of corporate expenses for reimbursement. There are three primary user interactions:Inputting expenses: The user says I just took a $25 cab to the airport for a client visit, billable to XXX client.Viewing expenses: The users says Did I forget to add that dinner from last Tuesday? Let me see if this is the one I'm thinking of...Generating reports: The user says Compile these 15 expenses into the XML report I need to submit these to finance for reimbursement.The report itself is pretty standard xml, with one node per expense, with each field given its own node (roughly, <trxs><trx><date>YYYY-MM-DD</date>...</trx><trx>...</trx><trxs>). The key complication is that depending on the type of expense, there are 1 or 2 additional custom fields per transaction. E.g., airfare expenses require a ticket_number, taxi expenses require destination and purpose fields, etc. These fields show up in the XML report roughly as <custom>%%%destination%%%Airport%%%purpose%%%client travel%%%</custom>The XML schema for all expense types is thus the same.Current solutionThere is a single Transaction model, which mirrors the XML schema and has fields like item_date, amount, client_code, employee_id, etc.  It also has a custom field, which stores the custom fields, serialized the same way they are serialized in the XML report.In order to keep the custom field seralization/deserialization code near the model, I've used Django proxy models to create subclasses that map to the same table but provide slightly different functionality.from django.db import modelsclass Transaction(models.Model):    MEAL = '5'    # ...    TAXI = '2'    EXPENSE_TYPE_CHOICES = (        (MEAL, 'Meal'),        # ...        (TAXI, 'Taxi'),    )    date_created = models.DateTimeField(auto_now_add=True)    item_date = models.DateField()    expense_type = models.CharField(choices=EXPENSE_TYPE_CHOICES, max_length=4)    # ...    custom = models.CharField(max_length=100)    def as_subclass(self):        if self.expense_type == self.MEAL:            self.__class__ = MealTrx        elif self.expense_type == self.TAXI:            self.__class__ = TaxiTrx        # ...        else:            raise ValueError(Expense type not set!)class MealTrx(Transaction):    objects = TypedManager({'expense_type': Transaction.MEAL})    # TypedManager adds .filter(**kwargs) to .get_queryset()    class Meta:        proxy = True    detail_re = re.compile(^%%%attendees%%%(.*)%%%purpose%%%(.*)$)    detail_str = %%%attendees%%%{}%%%purpose%%%{}    @property    def attendees(self): # Comma-separated list of attendees        match =  self.detail_re.match(self.custom)        if match is None:            return []        return match.group(1).split(, )    @attendees.setter    def attendees(self, value):        args = (, .join(value), self.purpose)        self.custom = self.detail_str.format(*args)    @property    def purpose(self):        match =  self.detail_re.match(self.custom)        if match is None:            return         return match.group(2)    @purpose.setter    def purpose(self, value):        args = (, .join(self.attendees), value)        self.custom = self.detail_str.format(*args)How the code gets called1. Creating expensesThis takes place inside the new transaction form save() method. The attributes are assigned and the model is saved. The property setters ensure that the fields are properly serialized:def save(self):    trx = base_form.save(commit=False)  # base_form is a ModelForm for Transaction    trx.as_subclass()    if trx.expense_type == models.Transaction.MEAL:        trx.attendees = self.meal_form.cleaned_data['attendees']        trx.purpose = self.meal_form.cleaned_data.get('purpose', )    elif trx.expense_type == models.Transaction.TAXI:    # ...    trx.save()    return trx2. Viewing expensesThis takes place inside template code, where the approach is to test for all of the possible custom fields:def expense_detail(request, expense_id):    expense = get_object_or_404(models.Transaction, pk=expense_id)    expense.as_subclass()    if expense.user != request.user:        LOGGER.warning(%r tried to see details of Transaction %s,                       request.user.username, expense_id)        raise http.Http404    return render(request, 'expenses/detail.html', {'expense': expense})and then inside the Jinja2 template{%- if expense.attendees -%}  <div class=table-row>    <span class=table-cell>Attendees</span>    <div class=table-cell>      <ul class=attendee-list>        {%- for attendee in expense.attendees -%}          <li>{{ attendee }}</li>        {%- endfor -%}      </ul>    </div>  </div>{%- endif -%}3. Generating reportsThis is the most straightforward piece, since the database schema mirrors the XML schema. Roughly speaking:class Report:    # snip    def to_string(self, encoding='utf-8'):        # build the document root, header info, etc.        for trx in self. transactions:            transactions.append(self.trx_as_element(trx))        return ET.tostring(root, encoding=encoding)    def trx_as_element(self, trx):        root = ET.Element('doc_trans')        items = [            ('item_date', transaction.item_date.strftime(%d-%b-%Y)),            ('expense_type', transaction.expense_type),            # ...            ('custom', transaction.custom),        ]        for key, val in items:            ET.SubElement(root, key).text = val        return rootIdentified issuesAdding a new expense type requires modifying a lot of placesThere's a lot of repetition in the definition of propertiesI'm not super-comfortable with the assignment of __class__ in as_subclass(). That feels too magic, and asking client code to remember to call as_subclass() seems like a bad ideaAccessing and modifying the custom properties is expensive (I think), requiring 1 or 2 regex matches and string interpolation every timeBottom-line: Is this code readable? Understandable? Easy to use? Would adding a new expense type, or creating a view to edit existing expenses be unduly painful?
Mapping different classes to the same database table
python;object oriented;design patterns;database;django