Class: ObjectidColumns::ObjectidColumnsManager
- Inherits:
-
Object
- Object
- ObjectidColumns::ObjectidColumnsManager
- Defined in:
- lib/objectid_columns/objectid_columns_manager.rb
Overview
The ObjectidColumnsManager does all the real work of the ObjectidColumns gem, in many ways – it takes care of reading ObjectId values and transforming them to objects, transforming supplied data to the right format when writing them, handling primary-key definitions and queries.
This is a separate class, rather than being mixed into the actual ActiveRecord class, so that we can add methods and define constants here without polluting the namespace of the underlying class.
Constant Summary collapse
- BINARY_OBJECTID_LENGTH =
NOTE: These constants are used in a metaprogrammed fashion in #has_objectid_columns, below. If you rename them, you must change that, too.
12- STRING_OBJECTID_LENGTH =
24
Instance Method Summary collapse
-
#has_objectid_columns(*columns) ⇒ Object
(also: #has_objectid_column)
Declares one or more columns as containing ObjectId values.
-
#has_objectid_primary_key(primary_key_name = nil) ⇒ Object
Declares that this class is using an ObjectId as its primary key.
-
#initialize(active_record_class) ⇒ ObjectidColumnsManager
constructor
Creates a new instance.
-
#read_objectid_column(model, column_name) ⇒ Object
Called from ObjectidColumns::HasObjectidColumns#read_objectid_column – given a model and a column name (which must be an ObjectId column), returns the data in it, as an ObjectId.
-
#to_valid_value_for_column(column_name, value) ⇒ Object
Given a value for an ObjectId column – could be a String in either hex or binary formats, or an ObjectId object – returns a String of the correct type for the given column (i.e., either the binary or hex String representation of an ObjectId, depending on the type of the underlying column).
-
#translate_objectid_query_pair(query_key, query_value) ⇒ Object
Given a key in a Hash supplied to
wherefor the given ActiveRecord class, returns a two-element Array consisting of the key and the proper value we should actually use to query on that column. -
#write_objectid_column(model, column_name, new_value) ⇒ Object
Called from ObjectidColumns::HasObjectidColumns#write_objectid_column – given a model, a column name (which must be an ObjectId column) and a new value, stores that value in the column.
Constructor Details
#initialize(active_record_class) ⇒ ObjectidColumnsManager
Creates a new instance. There should only ever be a single instance for a given ActiveRecord class, accessible via ObjectidColumns::HasObjectidColumns.objectid_columns_manager.
18 19 20 21 22 23 24 25 26 27 28 29 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 18 def initialize(active_record_class) raise ArgumentError, "You must supply a Class, not: #{active_record_class.inspect}" unless active_record_class.kind_of?(Class) raise ArgumentError, "You must supply a Class that's a descendant of ActiveRecord::Base, not: #{active_record_class.inspect}" unless superclasses(active_record_class).include?(::ActiveRecord::Base) @active_record_class = active_record_class @oid_columns = { } # We use a DynamicMethodsModule to add our magic to the target ActiveRecord class, rather than just defining # methods directly on the class, for a number of very good reasons -- see the class comment on # DynamicMethodsModule for more information. @dynamic_methods_module = ObjectidColumns::DynamicMethodsModule.new(active_record_class, :ObjectidColumnsDynamicMethods) end |
Instance Method Details
#has_objectid_columns(*columns) ⇒ Object Also known as: has_objectid_column
Declares one or more columns as containing ObjectId values. After this call, they can be written using a String in hex or binary formats, or an ObjectId object; they will return ObjectId objects for values, and can be queried using any of the above (as long as you use the where(:foo_oid => ...) Hash-style syntax).
If you don’t pass in any column names, this will look for columns that end in _oid and assume those are ObjectId columns.
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 103 def has_objectid_columns(*columns) return unless active_record_class.table_exists? # Autodetect columns ending in +_oid+ if needed columns = autodetect_columns if columns.length == 0 columns = columns.map { |c| c.to_s.strip.downcase.to_sym } columns.each do |column_name| # Go fetch the column object from the ActiveRecord class, and make sure it's present and of the right type. column_object = active_record_class.columns.detect { |c| c.name.to_s == column_name.to_s } unless column_object raise ArgumentError, "#{active_record_class.name} doesn't seem to have a column named #{column_name.inspect} that we could make an ObjectId column; did you misspell it? It has columns: #{active_record_class.columns.map(&:name).inspect}" end unless [ :string, :binary ].include?(column_object.type) raise ArgumentError, "#{active_record_class.name} has a column named #{column_name.inspect}, but it is of type #{column_object.type.inspect}; we can only make ObjectId columns out of :string or :binary columns" end # Is the column long enough to contain the data we'll need to put in it? required_length = self.class.const_get("#{column_object.type.to_s.upcase}_OBJECTID_LENGTH") # The ||= is in case there's no limit on the column at all -- for example, PostgreSQL +bytea+ columns # behave this way. unless (column_object.limit || required_length + 1) >= required_length raise ArgumentError, "#{active_record_class.name} has a column named #{column_name.inspect} of type #{column_object.type.inspect}, but it is of length #{column_object.limit}, which is too short to contain an ObjectId of this format; it must be of length at least #{required_length}" end # Define reader and writer methods that just call through to ObjectidColumns::HasObjectidColumns (which, in # turn, just delegates the call back to this object -- the #read_objectid_column method below; the one on # HasObjectidColumns just passes through the model object itself). cn = column_name dynamic_methods_module.define_method(column_name) do read_objectid_column(cn) end dynamic_methods_module.define_method("#{column_name}=") do |x| write_objectid_column(cn, x) end # Store away the fact that we've done this. @oid_columns[column_name] = column_object.type end end |
#has_objectid_primary_key(primary_key_name = nil) ⇒ Object
Declares that this class is using an ObjectId as its primary key. Ordinarily, this requires no arguments; however, if your primary key is not named id and you have not yet told ActiveRecord this (using self.primary_key = :foo), then you must pass the name of the primary-key column.
Note that, unlike normal database-generated primary keys, this will cause us to auto-generate an ObjectId primary key value for a new record just before saving it to the database (ActiveRecord’s +before_create hook). ObjectIds are safe to generate client-side, and very difficult to properly generate server-side in a relational database. However, we will respect (and not overwrite) any primary key already assigned to the record before it’s saved, so if you want to assign your own ObjectId primary keys, you can.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 40 def has_objectid_primary_key(primary_key_name = nil) # The Symbol-vs.-String distinction is critical when dealing with old versions of ActiveRecord; for example, if # you say <tt>self.primary_key = :foo</tt> (instead of <tt>self.primary_key = 'foo'</tt>) to older versions of # ActiveRecord, you can end up with some seriously weird errors later (like models trying to save themselves with # _both_ a +:foo+ and a +'foo'+ attribute -- ick!). Boo, AR. primary_key_name = primary_key_name.to_s if primary_key_name pk = active_record_class.primary_key # Make sure we know what the primary key is! if (! pk) && (! primary_key_name) raise ArgumentError, "Class #{active_record_class.name} has no primary key set, and you haven't supplied one to .has_objectid_primary_key. Either set one before this call (using self.primary_key = :foo), or supply one to this call (has_objectid_primary_key :foo) and we'll set it for you." end pk = pk.to_s if pk # Initially, this was a simple +||=+ statement. However, older versions of ActiveRecord will return the string or # symbol +id+ for the primary key if you haven't set an explicit primary key, even if there is no such column on # the underlying table. Again, ick. if (! pk) || (primary_key_name && pk.to_s != primary_key_name.to_s) active_record_class.primary_key = pk = primary_key_name end # In case someone is using composite_primary_keys (http://compositekeys.rubyforge.org/). raise "You can't have an ObjectId primary key that's not a String or Symbol: #{pk.inspect}" unless pk.kind_of?(String) || pk.kind_of?(Symbol) # Declare our primary-key column as an ObjectId column. has_objectid_column pk # If it's not called just "id", we need to explicitly define an "id" method that correctly reads from and writes # to the table. unless pk.to_s == 'id' p = pk dynamic_methods_module.define_method("id") { read_objectid_column(p) } dynamic_methods_module.define_method("id=") { |new_value| write_objectid_column(p, new_value) } end # Allow us to autogenerate the primary key, if needed, on save. active_record_class.send(:before_create, :assign_objectid_primary_key) # Override a couple of methods that, if you're using an ObjectId column as your primary key, need overriding. ;) [ :find, :find_by_id ].each do |class_method_name| @dynamic_methods_module.define_class_method(class_method_name) do |*args, &block| if args.length == 1 && args[0].kind_of?(String) || ObjectidColumns.is_valid_bson_object?(args[0]) || args[0].kind_of?(Array) args[0] = if args[0].kind_of?(Array) args[0].map { |x| objectid_columns_manager.to_valid_value_for_column(primary_key, x) if x } else objectid_columns_manager.to_valid_value_for_column(primary_key, args[0]) if args[0] end super(args[0], &block) else super(*args, &block) end end end end |
#read_objectid_column(model, column_name) ⇒ Object
Called from ObjectidColumns::HasObjectidColumns#read_objectid_column – given a model and a column name (which must be an ObjectId column), returns the data in it, as an ObjectId.
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 149 def read_objectid_column(model, column_name) column_name = column_name.to_s value = model[column_name] return value unless value # in case it's nil # If it's not nil, the database should always be giving us back a String... unless value.kind_of?(String) raise "When trying to read the ObjectId column #{column_name.inspect} on #{inspect}, we got the following data from the database; we expected a String: #{value.inspect}" end # ugh...ActiveRecord 3.1.x can return this in certain circumstances return nil if value.length == 0 # In many databases, if you have a column that is, _e.g._, BINARY(16), and you only store twelve bytes in it, # you get back all 16 anyway, with 0x00 bytes at the end. Converting this to an ObjectId will fail, so we make # sure we chop those bytes off. (Note that while String#strip will, in fact, remove these bytes too, it is not # safe: if the ObjectId itself ends in one or more 0x00 bytes, then these will get incorrectly removed.) case objectid_column_type(column_name) when :binary then value = value[0..(BINARY_OBJECTID_LENGTH - 1)] when :string then value = value[0..(STRING_OBJECTID_LENGTH - 1)] else unknown_type(type) end # +lib/objectid_columns/extensions.rb+ adds this method to String. value.to_bson_id end |
#to_valid_value_for_column(column_name, value) ⇒ Object
Given a value for an ObjectId column – could be a String in either hex or binary formats, or an ObjectId object – returns a String of the correct type for the given column (i.e., either the binary or hex String representation of an ObjectId, depending on the type of the underlying column).
194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 194 def to_valid_value_for_column(column_name, value) out = value.to_bson_id unless ObjectidColumns.is_valid_bson_object?(out) raise "We called #to_bson_id on #{value.inspect}, but it returned this, which is not a BSON ID object: #{out.inspect}" end case objectid_column_type(column_name) when :binary then out = out.to_binary when :string then out = out.to_s else unknown_type(type) end out end |
#translate_objectid_query_pair(query_key, query_value) ⇒ Object
Given a key in a Hash supplied to where for the given ActiveRecord class, returns a two-element Array consisting of the key and the proper value we should actually use to query on that column. If the key does not represent an ObjectID column, then this will just be exactly the data passed in; however, if it does represent an ObjectId column, then the value will be translated to whichever String format (binary or hex) that column is using.
We use this in ObjectidColumns:;ActiveRecord::Relation#where to make the following work properly:
MyModel.where(:foo_oid => BSON::ObjectId('52ec126d78161f56d8000001'))
This method is used to translate this to:
MyModel.where(:foo_oid => "52ec126d78161f56d8000001")
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 222 def translate_objectid_query_pair(query_key, query_value) if (type = oid_columns[query_key.to_sym]) # Handle nil, false if (! query_value) [ query_key, query_value ] # +lib/objectid_columns/extensions.rb+ adds String#to_bson_id elsif query_value.respond_to?(:to_bson_id) v = query_value.to_bson_id v = case type when :binary then v.to_binary when :string then v.to_s else unknown_type(type) end [ query_key, v ] # Handle arrays of values elsif query_value.kind_of?(Array) array = query_value.map do |v| translate_objectid_query_pair(query_key, v)[1] end [ query_key, array ] # Um...what did you pass? else raise ArgumentError, "You're trying to constrain #{active_record_class.name} on column #{query_key.inspect}, which is an ObjectId column, but the value you passed, #{query_value.inspect}, is not a valid format for an ObjectId." end else [ query_key, query_value ] end end |
#write_objectid_column(model, column_name, new_value) ⇒ Object
Called from ObjectidColumns::HasObjectidColumns#write_objectid_column – given a model, a column name (which must be an ObjectId column) and a new value, stores that value in the column.
178 179 180 181 182 183 184 185 186 187 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 178 def write_objectid_column(model, column_name, new_value) column_name = column_name.to_s if (! new_value) model[column_name] = new_value elsif new_value.respond_to?(:to_bson_id) model[column_name] = to_valid_value_for_column(column_name, new_value) else raise ArgumentError, "When trying to write the ObjectId column #{column_name.inspect} on #{inspect}, we were passed the following value, which doesn't seem to be a valid BSON ID in any format: #{new_value.inspect}" end end |