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
Class Method Summary collapse
-
.for_table(table_name) ⇒ Object
See above.
-
.register_for_table(table_name, instance) ⇒ Object
ObjectidColumns::Arel::Visitors::ToSql needs to be able to figure out whether an ObjectId column is of binary or text format, in order to properly transform/quote the value it has.
Instance Method Summary collapse
-
#activerecord_class_has_no_real_primary_key? ⇒ Boolean
This method basically says: does our
active_record_class
have a primary key defined, for real? There are two reasons this is anything more than (!! active_record_class.primary_key
):. -
#assign_objectid_primary_key(model) ⇒ Object
Assigns a new ObjectId primary key to a brand-new model that’s about to be created, if needed.
-
#find_or_find_by_id(*args) ⇒ Object
Implements .find or .find_by_id for classes that have a primary key that has at least one ObjectId column in it; this takes care of handling both normal primary keys and composite primary keys.
-
#has_objectid_columns(*columns) ⇒ Object
(also: #has_objectid_column)
Declares one or more columns as containing ObjectId values.
-
#has_objectid_primary_key(*primary_keys_that_are_objectid_columns) ⇒ Object
Declares that this class is using an ObjectId as its primary key.
-
#initialize(active_record_class) ⇒ ObjectidColumnsManager
constructor
Creates a new instance.
-
#is_objectid_column?(column_name) ⇒ Boolean
Given the name of a column, tell whether or not it is an ObjectId column.
-
#net_oid_columns ⇒ Object
Returns the same thing as
oid_columns
, except merges in the ActiveRecord class’s superclass’s columns, if any. -
#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.
-
#read_objectid_primary_key(model) ⇒ Object
Given a model, returns the correct value for #id.
-
#set_primary_key_from!(primary_keys) ⇒ Object
If you haven’t specified a primary key on your model (using
self.primary_key=
), and you callhas_objectid_primary_key
, we want to tell the ActiveRecord model that that’s the new primary key. -
#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
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. -
#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.
-
#write_objectid_primary_key(model, new_value) ⇒ Object
Given a model, stores a new value for #id.
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 30 31 |
# 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) self.class.register_for_table(active_record_class.table_name, self) end |
Class Method Details
.for_table(table_name) ⇒ Object
See above. Given a table name, this returns the ObjectidColumnsManager for it, or nil
if none has been defined for that table.
46 47 48 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 46 def for_table(table_name) @_registered_instances[table_name] end |
.register_for_table(table_name, instance) ⇒ Object
ObjectidColumns::Arel::Visitors::ToSql needs to be able to figure out whether an ObjectId column is of binary or text format, in order to properly transform/quote the value it has. However, by the time the code gets there, we no longer have access to the ActiveRecord model at all. So, instead, we need an entry point to be able to find the ObjectidColumnsManager for a table by name. That’s .for_table, below; this is the method called at the end of the constructor of every ObjectidColumnsManager, registering the instance by table name.
39 40 41 42 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 39 def register_for_table(table_name, instance) @_registered_instances ||= { } @_registered_instances[table_name] = instance end |
Instance Method Details
#activerecord_class_has_no_real_primary_key? ⇒ Boolean
This method basically says: does our active_record_class
have a primary key defined, for real? There are two reasons this is anything more than (!! active_record_class.primary_key
):
-
In earlier versions of ActiveRecord (like 3.0.x), this will return
id
even if you haven’t set it and there is no column namedid
. -
The
composite_primary_keys
gem can make this an array instead.
57 58 59 60 61 62 63 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 57 def activerecord_class_has_no_real_primary_key? (! active_record_class.primary_key) || (active_record_class.primary_key == [ ]) || ( ([ [ 'id' ], [ :id ] ].include?(Array(active_record_class.primary_key))) && (! active_record_class.columns_hash.has_key?('id')) && (! active_record_class.columns_hash.has_key?(:id))) end |
#assign_objectid_primary_key(model) ⇒ Object
Assigns a new ObjectId primary key to a brand-new model that’s about to be created, if needed. This handles composite primary keys correctly.
80 81 82 83 84 85 86 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 80 def assign_objectid_primary_key(model) Array(model.class.primary_key).each do |pk_column| if is_objectid_column?(pk_column) && model[pk_column].blank? model.send("#{pk_column}=", ObjectidColumns.new_objectid) end end end |
#find_or_find_by_id(*args) ⇒ Object
Implements .find or .find_by_id for classes that have a primary key that has at least one ObjectId column in it; this takes care of handling both normal primary keys and composite primary keys.
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 124 def find_or_find_by_id(*args) primary_key = active_record_class.primary_key pk_length = primary_key.kind_of?(Array) ? primary_key.length : 1 # If we just have a single primary key, we flatten any input, just because that's exactly what base # ActiveRecord does... if pk_length == 1 args = args.flatten args = args.map { |x| to_valid_value_for_column(primary_key, x) if x } yield(*args) else # composite_primary_keys, however, requires that you pass each key as a single, separate argument to .find or # .find_by_id; we transform them here. keys = args.map do |key| new_key = [ ] key.each_with_index do |key_component, index| column = primary_key[index] new_key << if is_objectid_column?(column) to_valid_value_for_column(column, key_component) if key_component else key_component end end new_key end yield(*keys) end end |
#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.
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 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 229 def has_objectid_columns(*columns) return unless active_record_class.table_exists? # Autodetect columns ending in +_oid+ if needed columns = autodetect_columns_from(active_record_class.columns_hash.keys) 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_keys_that_are_objectid_columns) ⇒ 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.
This method handles composite primary keys, as provided by the composite_primary_keys
gem, correctly.
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 164 def has_objectid_primary_key(*primary_keys_that_are_objectid_columns) return unless active_record_class.table_exists? # First, normalize our set of primary keys that are ObjectId columns... primary_keys_that_are_objectid_columns = primary_keys_that_are_objectid_columns.compact.map(&:to_s).uniq # Now, see what all the primary keys are. If the user hasn't specified any primary keys on the class at all yet, # but has told us what they are, then we need to tell ActiveRecord what they are. all_primary_keys = if activerecord_class_has_no_real_primary_key? set_primary_key_from!(primary_keys_that_are_objectid_columns) primary_keys_that_are_objectid_columns else Array(active_record_class.primary_key) end # Normalize the set of all primary keys. all_primary_keys = all_primary_keys.compact.map(&:to_s).uniq # Let's make sure we have a primary key... raise ArgumentError, "Class #{active_record_class.name} has no primary key set, and you haven't supplied one to #has_objectid_primary_key" if all_primary_keys.empty? # If you didn't specify any ObjectId columns explicitly, use what we know about the class to figure out which # ones you mean. if primary_keys_that_are_objectid_columns.empty? if all_primary_keys.length == 1 primary_keys_that_are_objectid_columns = all_primary_keys else primary_keys_that_are_objectid_columns = autodetect_columns_from(all_primary_keys, true) end end # Make sure we have at least one ObjectId primary key, if we're in this method. raise "Class #{active_record_class.name} has no columns in its primary key that qualify as object IDs automatically; you must specify their names explicitly." if primary_keys_that_are_objectid_columns.empty? # Make sure all the columns the user named actually exist as columns on the model. missing = primary_keys_that_are_objectid_columns.select { |c| ! active_record_class.columns_hash.has_key?(c) } raise "The following primary-key column(s) do not appear to actually exist on #{active_record_class.name}: #{missing.inspect}; we have these columns: #{active_record_class.columns_hash.keys.inspect}" unless missing.empty? # Declare our primary-key column as an ObjectId column. has_objectid_column *primary_keys_that_are_objectid_columns # Override #id and #id= to do the right thing... dynamic_methods_module.define_method("id") do self.class.objectid_columns_manager.read_objectid_primary_key(self) end dynamic_methods_module.define_method("id=") do |new_value| self.class.objectid_columns_manager.write_objectid_primary_key(self, 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| objectid_columns_manager.find_or_find_by_id(*args) { |*new_args| super(*new_args, &block) } end end end |
#is_objectid_column?(column_name) ⇒ Boolean
Given the name of a column, tell whether or not it is an ObjectId column.
383 384 385 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 383 def is_objectid_column?(column_name) net_oid_columns.has_key?(column_name.to_sym) end |
#net_oid_columns ⇒ Object
Returns the same thing as oid_columns
, except merges in the ActiveRecord class’s superclass’s columns, if any.
389 390 391 392 393 394 395 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 389 def net_oid_columns out = { } if (socm = superclass_objectid_columns_manager) out = socm.net_oid_columns end out.merge(oid_columns) 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.
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 275 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 return value if ObjectidColumns.is_valid_bson_object?(value) # we can get this when reading the 'id' pseudocolumn # 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 #{active_record_class.name} ID=#{model.id.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 type = 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 |
#read_objectid_primary_key(model) ⇒ Object
Given a model, returns the correct value for #id. This takes into account composite primary keys where some columns may be ObjectId columns and some may not.
90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 90 def read_objectid_primary_key(model) pks = Array(model.class.primary_key) out = [ ] pks.each do |pk_column| out << if is_objectid_column?(pk_column) read_objectid_column(model, pk_column) else model[pk_column] end end out = out[0] if out.length == 1 out end |
#set_primary_key_from!(primary_keys) ⇒ Object
If you haven’t specified a primary key on your model (using self.primary_key=
), and you call has_objectid_primary_key
, we want to tell the ActiveRecord model that that’s the new primary key. This takes care of that, and handles the fact that this may be a composite primary key, too.
68 69 70 71 72 73 74 75 76 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 68 def set_primary_key_from!(primary_keys) if primary_keys.length > 1 active_record_class.primary_key = primary_keys.map(&:to_s) elsif primary_keys.length == 1 active_record_class.primary_key = primary_keys[0].to_s else # nothing here; we handle this elsewhere end 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).
321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 321 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")
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 349 def translate_objectid_query_pair(query_key, query_value) if (type = net_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.
305 306 307 308 309 310 311 312 313 314 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 305 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 |
#write_objectid_primary_key(model, new_value) ⇒ Object
Given a model, stores a new value for #id. This takes into account composite primary keys where some columns may be ObjectId columns and some may not.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/objectid_columns/objectid_columns_manager.rb', line 106 def write_objectid_primary_key(model, new_value) pks = Array(model.class.primary_key) if pks.length == 1 write_objectid_column(model, pks[0], new_value) else pks.each_with_index do |pk_column, index| value = new_value[index] if is_objectid_column?(pk_column) write_objectid_column(model, pk_column, value) else model[pk_column] = value end end end end |