Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rediscovering ActiveRecord

Rediscovering ActiveRecord

Being a Rails developer is more than just understanding how to use the Framework to develop applications.

To become an efficient developer, you should learn how the Framework works; how deep this understanding should be is up to you. Exploring the Framework code is something that everyone should do at least once.

Not only may you learn how it works but also, you might learn new tricks from the code itself or discover small features that are not widely publicized.


Mario Alberto Chávez

May 04, 2016

More Decks by Mario Alberto Chávez

Other Decks in Programming


  1. michelada.io Rediscovering ActiveRecord Mario Alberto Chávez @mario_chavez mario@michelada.io

  2. michelada.io ActiveRecord is Magic

  3. michelada.io ActiveRecord is not Magic

  4. michelada.io Lets explore ActiveRecord

  5. michelada.io $ rails g model user name email $ rails

    db:migrate db:seed
  6. michelada.io class User < ActiveRecordBase end

  7. michelada.io > User.find 1 => #<User:0x007f9ea552a6e8 id: 1, name: "Marco

    Polo", email: “marco@explorer.it”,...>
  8. michelada.io But how?

  9. michelada.io Introspection

  10. michelada.io Load schema

  11. michelada.io > User.attribute_types

  12. michelada.io # activerecord/lib/active_record/model_schema.rb
 def load_schema! @columns_hash = connection. schema_cache. columns_hash(table_name).

    except(*ignored_columns) ... end
  13. michelada.io Our connection keeps a columns hash per each table

  14. michelada.io # activerecord/lib/active_record/ connection_adapters/schema_cache.rb # Get the columns for a

    table def columns(table_name) @columns[table_name] ||= connection.columns(table_name) end
  15. michelada.io # activerecord/lib/active_record/ connection_adapters/schema_cache.rb def columns_hash(table_name) @columns_hash[table_name] ||= Hash[columns(table_name).map do

    |col| [col.name, col] end ] end
  16. michelada.io > User.connection. schema_cache.columns_hash(“users") => {“id”=> #<ActiveRecord::ConnectionAdapters::Column: 0x007f944d27bb48 @name=“id", @sql_type_metadata=#<ActiveRecord::ConnectionAda

    pters::SqlTypeMetadata:0x007f944d298720 @sql_type="INTEGER", @type=:integer, ...}
  17. michelada.io We need a cast type

  18. michelada.io PostgreSQLColumn with sql_type “integer” ActiveModel::Type::Integer

  19. michelada.io A cast type is a class derived from ActiveModel::Type::Value

  20. michelada.io ActiveModel::Type::Value #deserialize

  21. michelada.io ActiveModel::Type::Value #cast

  22. michelada.io ActiveModel::Type::Value #serialize

  23. michelada.io All this is part of the Attributes API

  24. michelada.io Finally our model is aware of its schema

  25. michelada.io > User.attribute_types => {"id"=>#<ActiveModel::Type::Integer: 0x007f9ea537b5e0 @limit=nil, @precision=nil, @range=-2147483648...2147483648, @scale=nil>,

    "name"=>#<ActiveModel::Type::String: 0x007f9ea5340c38 @limit=nil, @precision=nil, @scale=nil>, ...}
  26. michelada.io What now?

  27. michelada.io > User.find 1 => #<User:0x007f9ea552a6e8 id: 1, name: "Marco

    Polo", email: “marco@explorer.it”,...>
  28. michelada.io A query needs to be constructed

  29. michelada.io ActiveRecord will try to reuse an existent query

  30. michelada.io # lib/active_record/core.rb 
 statement = cached_find_by_statement(key) { | params|

    where(key => params.bind).limit(1) }
  31. michelada.io # lib/active_record/statement_cache.rb 
 def self.create(connection, block = Proc.new) relation

    = block.call Params.new bind_map = BindMap.new( relation.bound_attributes) query_builder = connection.cacheable_query( relation.arel) new query_builder, bind_map end
  32. michelada.io Our query will be converted into ARel’s AST

  33. michelada.io relation = block.call Params.new Arel::Node::Equality ActiveRecord::Relation:: QueryAttribute Arel::Nodes::BindParam ID

    ActiveRecord::Statement Cache::Substitute Arel::Attributes::Attribute
 ID Where clause
  34. michelada.io bind_map = BindMap.new( relation.bound_attributes) ActiveRecord::Relation:: QueryAttribute
 ID ActiveRecord::Attribute:: WithCastValue

    LIMIT = 1 Binds
  35. michelada.io query_builder = connection.cacheable_query( relation.arel) "SELECT \"users\".* FROM \"users\" WHERE

    \"users \".\"id\" = ? LIMIT ?"
  36. michelada.io At this point our query will cached for future

  37. michelada.io You made it up to this point with me!

  38. michelada.io We are almost there…

  39. michelada.io > User.find 1 => #<User:0x007f9ea552a6e8 id: 1, name: "Marco

    Polo", email: “marco@explorer.it”,...>
  40. michelada.io We are ready to execute the query

  41. michelada.io # lib/active_record/core.rb 
 record = statement.execute([id], self, connection).first

  42. michelada.io # lib/active_record/statement_cache.rb def execute(params, klass, connection) bind_values = bind_map.bind

    params sql = query_builder.sql_for( bind_values, connection) klass.find_by_sql( sql, bind_values, preparable: true) end
  43. michelada.io bind_values = bind_map.bind params ActiveRecord::Relation:: QueryAttribute
 ID = 1

    ActiveRecord::Attribute:: WithCastValue
 LIMIT = 1 Binds
  44. michelada.io sql = query_builder.sql_for( bind_values, connection) "SELECT \"users\".* FROM \"users\"

    WHERE \"users \".\"id\" = ? LIMIT ?"
  45. michelada.io klass.find_by_sql(sql, bind_values, preparable: true) result_set = connection.select_all(sanitize_sql(sql), 

    Load”, binds, preparable: preparable) result_set.map { |record| instantiate(record, column_types) }
  46. michelada.io #<ActiveRecord::Result:0x007f8d80282390 @column_types={}, @columns=["id", "name", "email", "created_at", "updated_at"], @hash_rows=nil, @rows=[[1,

    "Marco Polo", "marco@explorer.it", "2016-04-23 23:22:21.130353", "2016-04-24 00:20:22.870451"]]>
  47. michelada.io The instantiate method takes care of the final work

  48. michelada.io First it allocates the new instance and initialize attributes

  49. michelada.io def init_with(coder) coder = LegacyYamlAdapter.convert( self.class, coder) @attributes =

    coder['attributes'] init_internals @new_record = coder['new_record'] self.class.define_attribute_methods _run_find_callbacks _run_initialize_callbacks self end
  50. michelada.io Define all accessors and helper methods for attributes

  51. michelada.io lib/active_record/ attribute_methods.rb file includes all the logic

  52. michelada.io Result Set is converted into ActiveRecord::LazyAttributeHash, this is used

    to set model’s attributes
  53. michelada.io ActiveRecord::LazyAttributeHash
 @types= {"id"=>#<ActiveModel::Type::Integer: 0x007ff9136b6ea8 @limit=nil, @precision=nil, @range=-2147483648...2147483648, @scale=nil>,...}, @values=

    {"id"=>1, "name"=>"Marco Polo”,...}
  54. michelada.io ActiveRecord::LazyAttributeHash delays attributes casting into the ActiveModel::Type

  55. michelada.io ActiveRecord::LazyAttributeHash
 @delegate_hash= {"id"=> #<ActiveRecord::Attribute::FromDatabase:... @type= #<ActiveModel::Type::Integer:..., @value=1, @value_before_type_cast=1>}

    {"id"=>#<ActiveModel::Type::Integer: 0x007ff9136b6ea8,...}, @values= {"id"=>1,...}
  56. michelada.io Finally!

  57. michelada.io > User.find 1 => #<User:0x007f9ea552a6e8 id: 1, name: "Marco

    Polo", email: “marco@explorer.it”,...>
  58. michelada.io What we have learned? •There is no magic in

    ActiveRecord •It caches and is lazy as much as possible •Following the code is not hard
  59. michelada.io Thanks! Mario Alberto Chávez @mario_chavez