Updating a property set as the key in DataMapper - ruby

Is it possible to update a property in DataMapper if :key is set to true?
Say, for example, I have a model set up like this:
class Post
include DataMapper::Resource
property :slug, Text, :unique => true, :key => true
# ...
end
and I made a new instance of this with :slug => "example-post-title".
I tried to update it by accessing the stored
#post = Post.get("example-post-title")
#=> #<Post #slug="example-post-title" ...>
#post.slug = "example-post-title-2"
#=> "example-post-title-2"
#post.save
#=> true
#post = Post.get("example-post-title-2")
#=> nil
but as you can see the slug was never updated. I also tried using the Post#update method:
#post = Post.get("example-post-title")
#=> #<Post #slug="example-post-title" ...>
#post.update(:slug => "example-post-title-2")
#=> true
#post = Post.get("example-post-title-2")
#=> nil
Looking in the database, the index column is not changed by either of these examples. It remains as example-post-title rather than example-post-title-2.
According to the docs, the Post#update method, similar to the Post#save method, should return true if the operation was successful, and false if it was not. It is returning true here, but it's not actually updating the record.
I've searched and searched and can't find anything about it on the Internet. Neither StackOverflow nor the DataMapper rdoc had anything about updating the key.
I know that I can have a unique Serial property and just get instances of Post using the slug (as in, make the Serial property the key instead of the slug), but I'm looking for a way to do it without that, if at all possible.

My hunch is that you can't update a key. According to the doc, they are protected against mass assignment:
Natural Keys are protected against mass-assignment, so their setter= will need to be called individually if you're looking to set them.
They don't talk about updating them but usually in "key => value" stores it is impossible or deprecated to update the key. I'd assume that's the case here as well, even though I can't find any hard evidence to give to you : /

Related

Can I check the validity of a single DataMapper property?

In a custom DataMapper setter, I'd like to check whether the value I'm setting is valid or not.
For instance:
class ToastMitten
include DataMapper::Resource
property :id, Serial
property :wearer, Enum['Chuck Norris', 'Jon Skeet']
property :first_worn_at, DateTime
def wearer=(name)
super
if wearer.valid? # How can I do this?
first_worn_at = Time.now
end
end
end
t = ToastMitten.new
t.wearer = 'Nathan Long' # invalid value; do NOT set first_worn_at
t.wearer = 'Jon Skeet' # valid value; set first_worn_at
Can I check the validity of a single property like this without calling valid? on the object itself and looking through all the errors?
I'm trying to figure this out myself, here is the best solution I've found so far:
While I haven't found a method to check the validity of a single property, as in:
t.wearer.valid?
I have found that you can check the validity of the entire object prior to saving, and then check if there are errors on the property you are interested in like so:
if t.valid?
# Everything is valid.
else
# There were errors, let's see if there were any on the 'wearer' property...
puts t.errors.on(:wearer)
end
I know that that isn't necessarily the answer you seek, but it's the best I've come up with so far. I'll post back if I find something better.

Sinatra and Datamapper not able to save an array to the datatype Object

I have an HTML form which uses the following Sinatra code to handle POST for the url '/add-artist':
post '/add-artist' do
if logged_in?
a = Artist.new
a.name = params[:name]
a.website = params[:website]
a.facebook = params[:facebook]
a.created_by = session[:user_id]
a.created_at = Time.now
a.updated_by = session[:user_id]
a_updated_at = Time.now
a.views = 0
a.save
#user = User.get session[:user_id]
#user.artists.push(a.id)
#user.save
redirect '/'
end
end
The object 'a' is being saved but '#user' is not. I guess more specifically, the value '#user.artists' is not being updated. If you need more info, please ask but I have a feeling that you Ruby vets will find the problem in the code I provided.
UPDATE
Here's some additional info. I was able to reproduce the error in irb. First here's my class definition for 'User'.
# dm_models.rb
require 'data_mapper'
DataMapper::setup(:default, "sqlite3://#{Dir.pwd}/event_review.db")
class User
include DataMapper::Resource
property :id, Serial
property :email, String
property :password, String
property :user_name, String
property :birthdate, Date
property :city, String
property :state, String
property :zip, String
property :geodata, Object
property :bio, Text
property :friends, Object
property :events, Object
property :event_reviews, Integer
property :artists, Object
property :artist_reviews, Integer
property :venues, Object
property :venue_reviews, Integer
property :created_at, DateTime
property :updated_at, DateTime
property :views, Integer
has n, :reviews
end
Here is the irb
>> require 'sinatra'
=> true
>> require 'data_mapper'
=> true
>> require './models/dm_models.rb'
=> true
>> require 'geokit'
=> true
>>
?> include Geokit::Geocoders
=> Object
>> u = User.get 8
=> #<User #id=8 #email="km#km.com" #password="km" #user_name="Katherine Miller" #birthdate=#<Date: 4895485/2,0,2299161> #city="Burbank" #state="CA" #zip="91501" #geodata=#<Geokit::GeoLoc:0x10150d4d8 #street_number=nil, #suggested_bounds=#<Geokit::Bounds:0x10150cf88 #sw=#<Geokit::LatLng:0x10150cd80 #lng=-118.315043, #lat=34.1766949>, #ne=#<Geokit::LatLng:0x10150cee8 #lng=-118.27996, #lat=34.221666>>, #lng=-118.2935891, #zip="91501", #state="CA", #precision="zip", #province=nil, #all=[#<Geokit::GeoLoc:0x10150d4d8 ...>], #street_address=nil, #provider="google", #city="Burbank", #lat=34.2039087, #country_code="US", #full_address="Burbank, CA 91501, USA", #street_name=nil, #accuracy=5, #country="USA", #success=true> #bio=<not loaded> #friends=[] #events=["13", "14", "15", "16", "28", "29"] #event_reviews=7 #artists=[] #artist_reviews=1 #venues=[] #venue_reviews=0 #created_at=#<DateTime: 70729968253/28800,-5/24,2299161> #updated_at=#<DateTime: 1178838019/480,-5/24,2299161> #views=56>
>>
?> u.artists
=> []
>> u.artists.push "5"
=> ["5"]
>> u.save
=> true
>> u = User.get 8
=> #<User #id=8 #email="km#km.com" #password="km" #user_name="Katherine Miller" #birthdate=#<Date: 4895485/2,0,2299161> #city="Burbank" #state="CA" #zip="91501" #geodata=#<Geokit::GeoLoc:0x1014e8638 #street_number=nil, #suggested_bounds=#<Geokit::Bounds:0x1014e80e8 #sw=#<Geokit::LatLng:0x1014e7eb8 #lng=-118.315043, #lat=34.1766949>, #ne=#<Geokit::LatLng:0x1014e8048 #lng=-118.27996, #lat=34.221666>>, #lng=-118.2935891, #zip="91501", #state="CA", #precision="zip", #province=nil, #all=[#<Geokit::GeoLoc:0x1014e8638 ...>], #street_address=nil, #provider="google", #city="Burbank", #lat=34.2039087, #country_code="US", #full_address="Burbank, CA 91501, USA", #street_name=nil, #accuracy=5, #country="USA", #success=true> #bio=<not loaded> #friends=[] #events=["13", "14", "15", "16", "28", "29"] #event_reviews=7 #artists=[] #artist_reviews=1 #venues=[] #venue_reviews=0 #created_at=#<DateTime: 70729968253/28800,-5/24,2299161> #updated_at=#<DateTime: 1178838019/480,-5/24,2299161> #views=56>
>> u.artists
=> []
>> u.artists.class
=> Array
The description of the above code: I retrieve user with id==8, push the value "5" into it. This appears to be successful. I save user#8. Then I re-retrieve user#8 and look at the artists value and it is an empty array.
And finally, I am able to update other fields like "artist_reviews". Is this because I am defining the datatype to be 'Object' for artists, events and venues? This problem exists for all of those fields.
Thanks for the help.
What do the logs say? Can you push to #user.artists? Is it an Array? It might fail validation and you cannot complete save.
I asked this a while ago but I'm pretty certain the solution is to serialize the object. In this case it was an array of integers. I'm not sure if I'll have the time to update this with a detailed solution but an array cannot be stored directly in a relational database. The array object must be 'serialized', essentially converted to a string. In this example the datatype for the artist attribute would then be text.
You could manually convert the artist column to an array (from string) push the new integer(s) into the array and then convert back to string and save. I'm assuming there is an automated way to do this but that's the idea.
Furthermore, this entire example is a poor way to handle associations. The better way to do this is to have a one-to-many association where there is an ArtistUser table which has two columns artist_id and user_id. Every new association is represented as a new row in the ArtistUser table.

why would .is_a? and .class give conflicting results?

I have three objects that are all the same class. One was created via Item.new and the other two were pulled from the database (Mongoid). I'm passing one/any of these objects to another method and checking the type in that method via is_a?:
def initialize (item, attrs = nil, options = nil)
super(attrs, options)
raise 'invalid item object' unless item.is_a?(Item)
Well, this raise is getting hit. So I check the class, is_a and instance_of in rails console. I'm getting conflicting results. Why would they have the same class but only one of them be an instance_of that class?
>> i0.is_a? Item
=> false
>> i1.is_a? Item
=> false
>> i2.is_a? Item
=> true
>> i0.class
=> Item
>> i1.class
=> Item
>> i2.class
=> Item
>> i0.instance_of?(Item)
=> false
>> i1.instance_of?(Item)
=> false
>> i2.instance_of?(Item)
=> true
Is there a better way to do this type checking of my inputs? Why would three things that are the same class not all be instances of that class?
I don't know Mongoid, but usually, in a DB access library, you don't get the actual object out of the database but rather a proxy object that acts as a stand-in for the object stored in the DB. Since Ruby lacks the features to implement a perfect transparent proxy, you will sometimes see odd results, especially when using reflection or around object identity.
Inspired on the #KL-7 comment, it must be happening sort of that:
class Item; end
class PseudoItem; end
# PseudodItem think it's an Item:
class << PseudoItem
def inspect
'Item'
end
end
i0 = Item.new
i1 = PseudoItem.new
i0.class #=> Item (correct!)
i1.class #=> Item (wrong, due to redefinition of inspect!)
i0.is_a? Item #=> true
i1.is_a? Item #=> false, as it is a PseudoItem
Ya, same problem here...
Problem resolved (bypassed) with am ugly:
i0.class.to_s==Item.to_s

Chaining datamapper relationships across different repositories

class A
include DataMapper::Resource
def self.default_repository_name
:alt_db
end
property :aid, Integer, :key => true
# other stuff
belongs_to :b, :model => 'B', :child_key => [ :bid ]
end
class B
include DataMapper::Resource
# this one is in the default repo
property :bid, Integer, :key => true
# other stuff
belongs_to :c, :model => 'C', :child_key => [ :cid ]
end
class C
include DataMapper::Resource
# this one is in the default repo
property :cid, Integer, :key => true
# other stuff
end
If I just have A and B, this works fine. If I add C, however, I get an error:
dm-core/model/property.rb:73:in `new': wrong number of arguments (4 for 3) (ArgumentError)
If I want to make a chain of relationships with DataMapper, so that I can give an ID in one place and get a piece of data that's, say, four tables away through a series of references to subsequent tables' primary key ID field, how can I do this?
EDIT: Digging into the DM source from the stack trace:
DataMapper.repository(other_repository_name) do
properties << klass.new(self, name, options, type)
end
That's where the error is raised. Indeed, in this case klass is a DataMapper Integer property, and it's initialize method only accepts three options (model, name, and an options hash).
This whole block is only executed because I'm using more than one repository, though B and C are in the same one so I don't know if that sheds any light on why it's erroring on the cid property.
EDIT2:
I have tried all permutations, and it appears that when you're chaining, once you cross a database-boundary, that must be the end of the chain. For example, since A is :alt_db and B is :default, B is as deep as I can go, regardless of whether C is :default, :alt_db, or a third option.
If instead both A and B were :default, or both were :alt_db, and then C were the opposite one, C would be as deep as I could go.
I don't understand this behavior really, though.
You found a bug actually. It's been fixed in master. You can try grabbing sources from git and see if it works.
Your code works fine for me.
irb(main):001:0> A.first.b.c
DEBUG - "(0.001168) SELECT "aid", "bid" FROM "as" ORDER BY "aid" LIMIT 1"
DEBUG - "(0.000337) SELECT "bid", "cid" FROM "bs" WHERE "bid" = 2 LIMIT 1"
DEBUG - "(0.000046) SELECT "cid" FROM "cs" WHERE "cid" = 3 LIMIT 1"
=> #<C #cid=3>
My gem is dm-core-1.1.0, you should check your version.
It turns out this was a small issue with DataMapper chaining across repositories. Submitted to them and it's allegedly been fixed already!
http://datamapper.lighthouseapp.com/projects/20609/tickets/1506-can-only-chain-up-to-first-time-changing-default-repository#ticket-1506-1

Are there any reserved key names in MongoMapper?

Could I declare a model with a key called :key, for instance? Is there any word I can't use for a key?
_id and _type. Also, any thing that would create a method the same as a mongomapper doc/edoc instance method, such as associations, etc.
The first question if very easy to answer yourself. Open irb and try:
>> require 'mongo_mapper'
=> true
>> MongoMapper.database = 'test'
=> "test"
>> class Test
>> include MongoMapper::Document
>> key :key
>> end
=> #<MongoMapper::Plugins::Keys::Key:0x101fc7a90 #default_value=nil, #type=nil, #name="key", #options={}>
>> t = Test.new(:key => 'value')
=> #<Test _id: BSON::ObjectID('4c4dcced7123374587000001'), key: "value">
>> t.save
=> true
>> Test.all
=> [#<Test _id: BSON::ObjectID('4c4dcced7123374587000001'), key: "value">]
No errors? I guess key is a valid key!
As far as I know, the only keys you shouldn't use for your own data are _id and _type. You can use either, but they will change behavior. Using _id will make whatever you're setting as that key the unique id for the object. Using _type will cause MongoMapper to try to instantiate an instance of whatever's in your _test key when bringing the object back from the database.
Here's a concrete example of John Nunemaker's answer.
I found out the hard way that the following tokens are referenced in your object's instance code and therefore will collide with any key of the same name (mongo_mapper/plugins/callbacks.b):
:destroy
:save
:create
:update
If you define
key :update, Integer
then you will be able to GET, DELETE, POST, but not PUT because that will try to call run_callbacks(:update), which has become nonsense at that point. I don't know how to fix that so I can have a field called "update" in my model. Anyone?
Follow-up: It seems the instance method that performs the actual update is also called :update, so it would not help to eliminate the use of these tokens for callbacks. Rather, this is just a case of colliding with an instance method that causes a much weirder error because it is used as a callback type FIRST, before being used as a method.

Resources