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.