Datamapper's hooks won't work - ruby

Can't understand why hooks don't work. I have the following model:
class DirItem
include DataMapper::Resource
# property <name>, <type>
property :id, Serial
property :dir_cat_id, Integer, :required => true
property :title, String, :required => true
property :price, Integer, :default => 0
belongs_to :dir_cat
has n, :dir_photos
has n, :dir_field_values
before :destroy do
logger.debug "==============DESTROYING ITEM ##{id}, TITLE
#{title}"
dir_field_values.destroy
dir_photos.destroy
end
end
When I call destroy method either from my app or irb, it returns false. The errors hash is empty, the log message doesn't print and the record won't delete.

This hook works for me (ruby 1.9.2 / DM 1.0.2):
require 'rubygems'
require 'dm-core'
require 'dm-migrations'
# setup the logger
DataMapper::Logger.new($stdout, :debug)
# connect to the DB
DataMapper.setup(:default, 'sqlite3::memory:')
class DirItem
include DataMapper::Resource
# property <name>, <type>
property :id, Serial
property :dir_cat_id, Integer, :required => true
property :title, String, :required => true
property :price, Integer, :default => 0
has n, :dir_photos
before :destroy do
dir_photos.destroy
end
end
class DirPhoto
include DataMapper::Resource
property :id, Serial
belongs_to :dir_item
end
DataMapper.finalize.auto_migrate!
#i = DirItem.create(:title => 'Title', :dir_cat_id => 1)
#i.dir_photos.create
#i.dir_photos.create
#i.dir_photos.create
#i.destroy
The DM logger reveals that each of the dir_photos are destroyed before the dir_item is. Instead of using hooks, you might want to look into using dm-constraints though. With something like:
has n, :dir_photos, :constraint => :destroy
you can be sure that all the dir_photos will be destroyed when the dir_item is destroyed, and this will also be enforced by database level foreign key constraints.

Related

Retrieve records which have many-to-many association, using ruby and datamapper

I'm learning Sinatra, and I have read datamapper documentation and found this n to n relationship example:
class Photo
include DataMapper::Resource
property :id, Serial
has n, :taggings
has n, :tags, :through => :taggings
end
class Tag
include DataMapper::Resource
property :id, Serial
has n, :taggings
has n, :photos, :through => :taggings
end
class Tagging
include DataMapper::Resource
belongs_to :tag, :key => true
belongs_to :photo, :key => true
end
What I understood from the code above is that one photo may have many or zero tags, and a tag may have many or zero photos. How do I retrieve a list of photos with the tags associated to it already loaded. I know datamapper uses the lazy approach, so it does not automatically loads the associated classes (in this case photo.tag). So this:
photos = Photo.all
would result in an array with Photo objects without the tags. Is there a way to automatically retrieve it or do I have to iterate over the array and set that manually?
Thanks in advance!
I also have a database which has similar relations. Author, Post, Tag are main models, and Subscribedtag and Tagging are built through has n, :through.
class Author
include DataMapper::Resource
property :id, Serial
property :email, String, :unique => true
property :password, String
property :first_name, String
property :last_name, String
property :bio, Text
property :phone, String, :unique => true
property :twitter, String, :unique => true
property :facebook, String, :unique => true
property :show_phone, Boolean, :default => false
property :show_facebook, Boolean, :default => false
property :show_twitter, Boolean, :default => false
property :is_admin, Boolean, :default => false
property :this_login, DateTime
property :last_login, DateTime
property :session_lasting, Integer, :default => 0
has n, :posts
has n, :subscribedtags
has n, :tags, :through => :subscribedtags
end
class Post
include DataMapper::Resource
property :id, Serial
property :title, String, :required => true
property :body, Text, :required => true
property :is_blog_post, Boolean, :default => true
property :viewed, Integer, :default => 0
property :featured, Boolean, :default => false
property :created_at, DateTime
property :updated_at, DateTime
belongs_to :author
belongs_to :category
has n, :comments
has n, :taggings
has n, :tags, :through => :taggings
validates_length_of :title, :min => 3
validates_length_of :body, :min => 20
validates_format_of :title, :with => /\w/
#some other methods
end
class Tag
include DataMapper::Resource
property :id, Serial
property :name, String, :unique => true
has n, :taggings
has n, :posts, :through => :taggings
has n, :subscribedtags
has n, :authors, :through => :subscribedtags
validates_length_of :name, :min => 1
validates_format_of :name, :with => /\w/
# some other methods
end
class Tagging
include DataMapper::Resource
belongs_to :tag, :key => true
belongs_to :post, :key => true
end
class Subscribedtag
include DataMapper::Resource
belongs_to :tag, :key => true
belongs_to :author, :key => true
end
The way you've defined models, allows you to write queries, like that.
2.2.0 :016 > kant = Tag.get(25) # getting tag instance with id 25 and assign it to variable named kant
=> #<Tag #id=25 #name="İmmanuil Kant">
2.2.0 :017 > kant.posts
=> #returns post instances which has this tag.
2.2.0 :018 > kant.posts.count # count of posts with this tag.
=> 2
2.2.0 :021 > kant.posts.first.author.first_name
=> "Ziya" # check my author class and first_name attribute.
Let's say I want to retrieve the tag instances which has no posts.
a simple ruby command.
2.2.0 :024 > Tag.each {|tnp| puts tnp.name if tnp.posts.count == 0}
Latın
Python
Ruby
Sosializm
Hegel
Or retrieving tags based on posts.
2.2.0 :034 > p = Post.get(9)
=> #<Post #id=9 #title="Why we use Lorem Ipsum" #body=<not loaded> #is_blog_post=false #viewed=0 #featured=false #created_at=#<DateTime: 2015-08-02T23:14:04+05:00 ((2457237j,65644s,0n),+18000s,2299161j)> #updated_at=#<DateTime: 2015-08-02T23:14:04+05:00 ((2457237j,65644s,0n),+18000s,2299161j)> #author_id=1 #category_id=1>
2.2.0 :035 > p.tags
=> [#<Tag #id=19 #name="Bundesliqa">]
retrieve posts which has no tag.
2.2.0 :043 > Post.each {|pnt| puts pnt.id if pnt.tags.count.zero?}
8 #post with id has no tags
2.2.0 :044 > Post.get(8).tags.count
=> 0
you can also query via other attributes.
2.2.0 :046 > Tag.first(:name => "Lorem").id
=> 30
iterate over results
2.2.0 :050 > Tag.first(:name => "Lorem").posts.each {|lorempost| puts lorempost.title} # printing post titles which are tagged with lorem.
Get'em all
qwerty
I also associated authors with tags through Subscribedtags model, which I can easily check which author is subscribed to which tag, and vice versa.
2.2.0 :055 > z = Author.get(1)
=> # returns details of author instance
2.2.0 :056 > z.tags
=> [#, #, #, #]
or querying via Subscribedtag
2.2.0 :057 > z.subscribedtags
=> [#<Subscribedtag #tag_id=2 #author_id=1>, #<Subscribedtag #tag_id=4 #author_id=1>, #<Subscribedtag #tag_id=25 #author_id=1>, #<Subscribedtag #tag_id=30 #author_id=1>]
you can also define your own functions to utilize querying. I've defined a subscribed_tags method which returns an array of subscribed tags' names.
2.2.0 :058 > z.subscribed_tags
=> ["Həyat", "Məstan", "İmmanuil Kant", "Lorem"]
If I want to retrieve the first_name attribute of a random author, who is subscribed to tag named "Lorem",
2.2.0 :062 > Tag.first(:name => "Lorem").authors.sample.first_name
=> "Ziya"
As an answer to your 2nd question, yes, most times you have to iterate.
Because Photos.all return a collection of Photo object instances. And this instances individually has tag attributes, not the array consists of Photo instances.
if you call p = Photo.all; print p.tags; it will return all tags associated with all photos, which may or may not be the thing you want.
Feel free to ask more questions, if these are not enough.

Get associated records with Datamapper using Sinatra

I am currently working on a small backend, managing events with associated locations. Unfortunately, it's my first time i work with Ruby/Sinatra/Datamapper. After 3 hours trying to find a solution, i have to write this post.
I have defined two Resources:
class Event
include DataMapper::Resource
property :id, Integer, :key => true
property :name, Text
property :description, Text
has 1, :location
end
class Location
include DataMapper::Resource
property :id, Integer, :key => true
property :name, Text
property :latitude, Float
property :longitude, Float
belongs_to :event
end
This is my route to list all events:
get "/events/" do
#events = Event.all
content_type :json
#events.to_json
end
Is there a easy way the get the location as a parameter in the output of the associated event object?
Thank you very much for your support!
require 'rubygems'
require 'sinatra'
require 'dm-core'
require 'dm-migrations'
require 'dm-sweatshop' # for fixtures
require 'json'
DataMapper::Logger.new($stdout, :debug)
DataMapper.setup(:default, 'sqlite::memory:')
class Event
include DataMapper::Resource
property :id, Serial # will automatically become an auto-increment key
property :name, String # defaults to being max 50 char length
property :description, Text, :lazy => false # defaults to true
belongs_to :location # instead of one-to-one relation, which are rarely useful
end
class Location
include DataMapper::Resource
property :id, Serial
property :name, String
property :latitude, Float # perhaps decimal with less precision would suffice
property :longitude, Float
has n, :events
end
DataMapper.finalize.auto_migrate!
# Define some fixtures to have some data to play around with
def rand_float(min, max); rand * (max - min) + min end
Location.fix {{
:name => /\w+/.gen,
:latitude => rand_float(40.0, 43.0),
:longitude => rand_float(4.8, 5.4)
}}
Event.fix {{
:name => /\w+/.gen,
:description => /[:sentence:]/.gen[5..100],
:location => Location.pick
}}
100.of { Location.gen; Event.gen }
# Search events by properties of its association
get "/events/:location_name" do |location_name|
#events = Event.all(Event.location.name => location_name)
#events.to_json
end
# Return both objects in the same array
get "/events/" do
#events = Event.map {|e| [e, e.location] }
#events.to_json
end
Finally found the answer by myself when i took a deeper look at the to_json options
#events.to_json(:relationships=>{:location=>{}})

How can I resolve the IllegalContextError when attempting to save a datamapper model?

When I try to this code, I get an IllegalContextError at the "self.save..." line. Can you tell me what I'm doing wrong?
I would just call the create method on Player without messing around with initialize, but I want a related week object to be created as part of the initialization.
require 'data_mapper'
DataMapper::setup(:default, "sqlite3://#{Dir.pwd}/prod.db")
class Player
include DataMapper::Resource
property :name, String, :key => true
property :sport, String
has n, :weeks
def initialize(name, sport, week)
self.save(:name => name, :sport => sport)
self.weeks.create(:id => "#{name}#{week}", :score => 0)
end
end
class Week
include DataMapper::Resource
property :id, String, :key => true
property :week, Integer
property :score, Integer
belongs_to :player
end
DataMapper.finalize.auto_migrate!
Player.new("jack", "golf", 5)
I understand that this is probably not the best way, so before you shoot my method down, please provide a better solution. I will probably accept your answer :)
It seems like the IllegalContextError is originating from the data_mapper validators.
The data_mapper docs on validators doesn't provide much info for a newbie to understand context AND in relation to validators.
Here is my hacky workaround. I override the validators by using the bang operator (!). The solution is as follows.
require 'data_mapper'
DataMapper::setup(:default, "sqlite3://#{Dir.pwd}/prod.db")
class Player
include DataMapper::Resource
property :name, String, :key => true
property :sport, String
has n, :weeks
def initialize(name, sport, week)
self[:name] = name
self[:sport] = sport
self[:week] = week
self.save!
self.weeks.create(:id => "#{name}#{week}", :score => 0)
end
end
class Week
include DataMapper::Resource
property :id, String, :key => true
property :week, Integer
property :score, Integer
belongs_to :player
end
DataMapper.finalize.auto_migrate!
Player.new("jack", "golf", 5)

Unable to destroy a parent object

I have a large and complicated User model that looks something like this:
class User
class Link
include DataMapper::Resource
property :id, Serial, :key => false
belongs_to :follower, :model => 'User', :key => true
belongs_to :followed, :model => 'User', :key => true
end
include DataMapper::Resource
property :id, Serial
property :username, String, :required => true
has n, :links_to_followers, :model => 'User::Link', :child_key => [:followed_id]
has n, :links_to_followed, :model => 'User::Link', :child_key => [:follower_id]
has n, :comments
has 1, :profile_image
end
My problem is that Datamapper is not letting me Destroy it. I thought this was a result of Datamapper not wanting to destroy an object with un-destroyed child objects so I put in a method destroy_deep that calls destroy on the links_to_followers, links_to_followed, underlying comments, and the profile image (these are all destroyed correctly).
However, even if I call user.destroy after that, the user is not destroyed. There are no error messages of any kind. Is there some kind of cascading delete command that I am missing?
I resolved this.
Apparently to debug destroy, object.errors isn't useful. Instead track exceptions like:
begin
u.destroy
rescue Exception => e
p e
end
The solution was that one of the children fields didn't map back to User. I had a class Like that belonged to User, but User didn't have n Likes.

App Engine Jruby DataMapper List Property

How to use list/array as property in DataMapper on Jruby on Google AppEngine?
This will work like has..and..belongs..to..many, without a join table...
class Person
include DataMapper::Resource
property :id, Serial
property :name, String, :nullable => false
property :project_ids, List
timestamps :at
# project should be flagged as archived, not deleted
def projects
Project.all(:id => project_ids)
end
end
class Project
include DataMapper::Resource
property :id, Serial
property :name, String, :nullable => false
property :archived, Boolean, :default => false
# the join table is bolted onto the person model
def people
Person.all(:project_ids => id)
end
end

Resources