find_or_create_xxx behaving differently than create

It appears that find_or_create_by_xxx is attempting type conversion where a simple create(...) does not. Bug?

Before going further, here's the schema and the model:

  create_table "weather_stations", :force => true do |t|     t.string "callsign"     t.decimal "lat", :precision => 9, :scale => 6     t.decimal "lng", :precision => 9, :scale => 6     t.decimal "elevation_m", :precision => 5, :scale => 1   end

class WeatherStation < ActiveRecord::Base   validates_presence_of :callsign, :lat, :lng   validates_uniqueness_of :callsign, :case_sensitive => false   acts_as_mappable   ... end

[Note the "validates_uniqueness_of :callsign, :case_sensitive => false" -- that comes into play here.]

** Assume 'specs' is a hash whose :callsign is a string:

specs

=> {:callsign=>"0N6", :lat=>39.0128889, :lng=>-75.5339722, :elevation_m=>15.0}

specs[:callsign].class

=> String

** The call to find_or_create_by_callsign fails -- note that id comes back nil and the other fields are not filled in:

w = WeatherStation.find_or_create_by_callsign(specs[:callsign], :lat => specs[:lat], :lng => specs[:lng])

=> #<WeatherStation id: nil, callsign: "0N6", domain: nil, created_at: nil, updated_at: nil, lat: nil, lng: nil, elevation_m: nil>

** The generated SQL shows a rollback:

  WeatherStation Load (22.2ms) SELECT * FROM `weather_stations` WHERE (`weather_stations`.`callsign` = '0N6') LIMIT 1   SQL (0.4ms) BEGIN   WeatherStation Load (230.6ms) SELECT `weather_stations`.id FROM `weather_stations` WHERE (LOWER(`weather_stations`.`callsign`) = BINARY '0n6') LIMIT 1   SQL (1.0ms) ROLLBACK

** Using a simple .create() instead works:

x = WeatherStation.create(:callsign => specs[:callsign], :lat => specs[:lat], :lng => specs[:lng])

=> #<WeatherStation id: 30774, callsign: "0N6", domain: nil, created_at: "2010-05-16 06:38:22", updated_at: "2010-05-16 06:38:22", lat: #<BigDecimal:8db79b0,'0.390128889E2',12(16)>, lng: #<BigDecimal:8db7828,'-0.755339722E2',12(16)>, elevation_m: nil>

** ...but looking at the SQL shows that it does the same check for uniqueness before inserting the row. Since the record isn't in the table, it's okay that it fails:

SQL (70.0ms) BEGIN   WeatherStation Load (365.4ms) SELECT `weather_stations`.id FROM `weather_stations` WHERE (LOWER(`weather_stations`.`callsign`) = BINARY '0n6') LIMIT 1   WeatherStation Create (2.0ms) INSERT INTO `weather_stations` (`callsign`, `domain`, `created_at`, `updated_at`, `lat`, `lng`, `elevation_m`) VALUES('0N6', NULL, '2010-05-16 06:38:22', '2010-05-16 06:38:22', 39.0128889, -75.5339722, NULL)   SQL (3.3ms) COMMIT

I don't understand enough about SQL's "BINARY '0n6'" -- is that the source of the trouble? Or is my validation bogus? Why is find_or_create_by_callsign() failing?

TIA.

- ff

I don't understand enough about SQL's "BINARY '0n6'" -- is that the source of the trouble? Or is my validation bogus? Why is find_or_create_by_callsign() failing?

I think it's far more likely that it's the nil attributes you noted that are the problem - they would certainly prevent the record from being saved. It appears that find_or_create_by doesn't quite work the way you expected it to - the code and unit tests suggests that you should be doing

WeatherStation.find_or_create_by_callsign(:callsign => specs[:callsign], :lat => specs[:lat], :lng => specs[:lng])

There is an another example in the docs of the usage you were attempting though, looks like it is just wrong/misleading.

Fred

Frederick Cheung wrote:

... It appears that find_or_create_by doesn't quite work the way you expected it to - the code and unit tests suggests that you should be doing

WeatherStation.find_or_create_by_callsign(:callsign => specs[:callsign], :lat => specs[:lat], :lng => specs[:lng])

You are correct -- that works as expected.

There is an another example in the docs of the usage you were attempting though, looks like it is just wrong/misleading.

Correct again! The line in the doc is:

  # No 'Summer' tag exists   Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")

which passes "Summer" rather than :name => "Summer".

Sigh. A guy I used to work with told me that there is only one "authoritative documentation", and that's the source code itself. Evidently, Rails subscribes to this philosophy as well.

Frederick Cheung wrote: > ...

> There is an another example in the docs of the usage you were > attempting though, looks like it is just wrong/misleading.

Correct again! The line in the doc is:

# No 'Summer' tag exists Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")

Actually that usage is correct too. The misleading example is actuall demonstrating a different point.

which passes "Summer" rather than :name => "Summer".

Sigh. A guy I used to work with told me that there is only one "authoritative documentation", and that's the source code itself. Evidently, Rails subscribes to this philosophy as well.

A fair amount of effort has gone into the rails documentation of late. Like the rest of rails it only gets better if people contribute

Frederick Cheung wrote:

A fair amount of effort has gone into the rails documentation of late. Like the rest of rails it only gets better if people contribute

Agreed. But, as evidenced by my sometimes boneheaded questions in this forum, Rails would be in serious trouble if I were a contributor! :slight_smile: (At least for now...)