ActiveJsonEntity and ActiveRecord’s shared DNA

New to ActiveJsonEntity? Check out the introduction.

As a new engineer at Wealthfront, I’ve been getting my hands dirty migrating our existing hand-written models to ActiveJsonEntity models. It’s been a tremendous way to get to know our existing codebase and appreciate the power of ActiveJsonEntity. Coming from a traditional Rails background, I wanted to make sure I had a deep understanding of where ActiveRecord and ActiveJsonEntity deviate before charging ahead. I figured what better way to accomplish this than by looking under their respective hoods and comparing implementations.

As a quick refresher, ActiveRecord generates its models by inspecting the schema of the corresponding database tables. At Wealthfront, our models are defined on the backend as Java classes. Much like ActiveRecord, Java’s reflection API enables us to examine every detail of a class, including things like type information, field definitions, and annotations. A Java class ultimately serves as the model’s blueprint.

Let’s dive in and take a look at how ActiveRecord and ActiveJsonEntity construct models.

ActiveRecord

Note: The examples below aim to provide insight into how ActiveRecord establishes a mapping between a model and an existing database table. However, ActiveRecord is designed to support multiple database adapters (MySql, MySql2, Postgresql, sqlite3) so some of the code snippets are from abstract classes and may be ultimately overridden by database adapter specific implementations.

ActiveRecord’s first step is to find a model’s corresponding database table by using its class name. There is some extra logic in compute_table_name to handle cases involving single table inheritance or abstract classes, but ActiveRecord generally follows a very straightforward naming convention. Table names are pluralized snake-case versions of model class names. For example, the user_stories table maps to the UserStory model.

def undecorated_table_name(class_name = base_class.name)
  table_name = class_name.to_s.demodulize.underscore
  pluralize_table_names ? table_name.pluralize : table_name
end

def compute_table_name
  base = base_class
  if self == base
    if parent < Base && !parent.abstract_class?
      contained = parent.table_name
      contained = contained.singularize if parent.pluralize_table_names
      contained += '_'
    end
    "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}
     #{table_name_suffix}"
  else
    base.table_name
  end
end

Now that ActiveRecord knows where to look, it can pull the table’s schema using the database adapter. Parsing the schema enables ActiveRecord to create a list of column objects. Columns represent model attributes and contain the name, data type, default value and other helpful information.

def columns(table_name):
  sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
  execute_and_free(sql, 'SCHEMA') do |result|
    each_hash(result).map do |field|
      new_column(field[:Field], field[:Default], field[:Type], 
                 field[:Null] == "YES", field[:Collation], field[:Extra])
    end
  end
end

Then ActiveRecord infers the attribute’s data type by checking the column’s field type, which was available in the schema.

def simplified_type(field_type)
  case field_type
  when /int/i
    :integer
  when /float|double/i
    :float
  when /decimal|numeric|number/i
    extract_scale(field_type) == 0 ? :integer : :decimal
  when /datetime/i
    :datetime
  when /timestamp/i
    :timestamp
  when /time/i
    :time
  when /date/i
    :date
  when /clob/i, /text/i
    :text
  when /blob/i, /binary/i
    :binary
  when /char/i, /string/i
    :string
  when /boolean/i
    :boolean
  end
end

Lastly, ActiveRecord typecasts the attribute by using the type returned from simplified_type to determine the appropriate helper method to pass the attribute’s value.

def type_cast(value)
  return nil if value.nil?
  return coder.load(value) if encoded?
  klass = self.class
  case type
  when :string, :text        then value
  when :integer              then klass.value_to_integer(value)
  when :float                then value.to_f
  when :decimal              then klass.value_to_decimal(value)
  when :datetime, :timestamp then klass.string_to_time(value)
  when :time                 then klass.string_to_dummy_time(value)
  when :date                 then klass.value_to_date(value)
  when :binary               then klass.binary_to_string(value)
  when :boolean              then klass.value_to_boolean(value)
  else value
  end
end

ActiveJsonEntity

With ActiveJsonEntity, the process starts by finding the corresponding java class. This is easy because it’s explicity passed to the with_java_entity method, which is called when the class is loaded.

class AJEUser
  include ActiveJsonEntity
  with_java_entity com.wealthfront.user.AJEUser
end

Then ActiveJsonEntity pulls the declared fields and instance methods of the Java object. These will be inspected to determine which fields should ultimately become attributes of the model.

def with_java_entity(java_entity)
  java_class = java_entity.java_class
  java_class.declared_fields.
             each_with_index(&method(:register_entity_value))
  java_class.declared_instance_methods.
             each_with_index(&method(:register_entity_value))
end

Most of the action happens by passing each field to register_entity_value.

def register_entity_value(field, field_idx)
  annotation = field.annotation(com.twolattes.json.Value.java_class)
  if annotation
    field_name = annotation.name.blank? ? field.name : annotation.name
    field_type = field.class == java.method ? field.return_type : field.type
    return if field_type.nil?

    case classify_field_type(field_type)
    when :boolean    then register_boolean(field_name)
    when :datetime   then register_datetime(field_name)
    when :collection then register_collection(field_name, field_idx)
    when :map        then register_map(field_name, field_idx)
    when :enum       then register_enum(field_name, field_type)
    when :entity     then register_sub_entity(field_name, field_type)
    else                  register_value_field(field_name)
    end
  end
end

First, the field’s annotation is checked to determine if the field was included when the Java object was serialized prior to being passed to the Rails layer. Only Java fields annotated with “@Value” get serialized in our system. Next, classify_field_type does its work by leveraging Java reflection. It’s very similar to ActiveRecord’s own simplified_type (check out the ActiveJsonEntity introduction for more on how the reflection API enables us to determine field types).

def classify_field_type(type)
  return :boolean    if type == java.lang.Boolean::TYPE || 
                        type == java.lang.Boolean.java_class
  return :datetime   if type == org.joda.time.DateTime.java_class
  return :collection if java.util.Collection.java_class.
                                             assignable_from?(type)
  return :map        if java.util.Map.java_class.assignable_from?(type)
  return :enum       if java.lang.Enum.java_class.assignable_from?(type)
  return :entity     if type.respond_to?(:annotation_present?) &&
                        type.annotation_present?(com.twolattes.
                                                 json.Entity.java_class)

end

Lastly, we check the field’s type and call the appropriate helper method to set the attribute’s type.

ActiveJsonEntity’s implementation may look a bit different on the surface due to the fact that it interacts with Java objects, but ActiveRecord and ActiveJsonEntity share a significant amount of DNA. This fact has been and will continue to be an incredible asset in helping new new rails developers like myself get to know the Wealthfront codebase and begin contributing immediately.