Datamapper - Get most associated N records for association ( ex: 5 most liked posts ) - ruby

I am working on my blog, a Sinatra application that uses Datamapper as its ORM. I just added a feature for hash-tags. A hash-tag is as such, its a many-to-many relationship with a 'Story'. My need is to get the most popular/used 5 hash-tags to display on the sidebar.
Here is an abstract of my modals that are of interest.
The hash-tag model
class Hashtag
include DataMapper::Resource
has n, :stories, through: Resource
def self.default_repository_name
:default
end
property :id, Serial
property :created_at, DateTime
property :updated_at, DateTime
property :hashtag, String
end
The story model:
# File: models/story.rb
class Story
include DataMapper::Resource
def self.default_repository_name
:default
end
belongs_to :person
belongs_to :category
has 1, :story_content
has n, :comments
has n, :hashtags, through: Resource
property :id, Serial
property :created_at, DateTime
property :updated_at, DateTime
property :published, Boolean, default: false
property :privacy_level, Enum[ :private, :friends, :public ] # SNS access control for Twitter/FB only
end
Need: The most efficient way to get the most used n hash-tags.
Tag 1 ~> 1 story
Tag 2 ~> 2 stories
Tag 3 ~> 3 stories
For n = 2, I need ~> [Tag 2, Tag 3]
I am thinking of maintaining a counter and did it for some thing like 'Likes'. Wonder if there is any other way than cache the count of Stories per HashTag.

Since the top tags are not very likely to change very frequently, you could alternatively compute and cache them for a longer period of time, for example a day. Then, you can recompute the top five from scratch in a nightly job.
Pros:
No extra stories_count column
No maintenance of the stories_count (not that easy to handle!)
Fast queryability
Cons:
Top 5 may be out of date (not an option if you expect frequent changes)
Need to build and configure a nightly job

Related

Create JSON from 2 associated Datamapper models

Here is my question.
I have 2 associated Datamapper models:
class Task
include DataMapper::Resource
property :id, Serial
property :date, Date
property :amount, Float
belongs_to :project, :required => true
end
class Project
include DataMapper::Resource
property :id, Serial
property :name, String, :required => true
property :desc, Text
belongs_to :company
has n, :tasks
end
My goal is to created JSON that will contain task date, amount and project name, that should be matched by project_id. At the moment JSON generation has following look:
Task.all.to_json(:only => [:date, :amount, :project_id])
I can access project_id from Task model, but have no idea how to add respective project name from Project model for every task. In SQL it looks like join:
select tasks.date, tasks.amount, projects.name from tasks
inner join projects
on tasks.project_id = projects.id;
Can you suggest correct way to create final JSON, using Datamapper way, but not SQL?
Thank you.
I have found solution for my problem. Here it is:
# create new structure to store merged result
Task_entry = Struct.new(:date, :amount, :pname)
# array to get results from database
all_task_items = Array.new
# run through result and fill the array with required data
Task.all.each do |task|
task_item = Task_entry.new(task.date, task.amount, task.project.name)
all_task_items << task_item
end
all_task_items.to_json # generate json
It works for me well. Hope it can be helpful.

One-to-one DataMapper association

I'm very new to DataMapper, and I'm trying to create models for the following scenario:
I've got a number of users (with a user name, password etc.), who can also be players or referees or both (so Single Table Inheritance is not an option). The base models would be:
class User
include DataMapper::Resource
property :id, Serial
# Other user properties go here
end
class Player
include DataMapper::Resource
property :id, Serial
# Other player properties go here
# Some kind of association goes here
end
class Referee
include DataMapper::Resource
property :id, Serial
# Other referee properties go here
# Some kind of association goes here
end
DataMapper.finalize
I'm not sure, though, what kinds of associations to add to Player and Referee. With belongs_to :user, multiple players can be associated with the same user, which doesn't make sense in my context. In RDBMS terms I guess what I want is a unique constraint on the foreign key in the Players and Referees tables.
How do I accomplish this in my DataMapper model? Do I have to perform the check myself in a validation?
There are different ways you could do this. Here's one option:
class User
include DataMapper::Resource
property :id, Serial
# Other properties...
has 1, :referee, :required => false
has 1, :player, :required => false
end
class Referee
include DataMapper::Resource
# DON'T include "property :id, Serial" here
# Other properties...
belongs_to :user, :key => true
end
class Player
include DataMapper::Resource
# DON'T include "property :id, Serial" here
# Other properties...
belongs_to :user, :key => true
end
Act on the referee/player models like:
u = User.create(...)
u.referee = Referee.create(...)
u.player = Player.create(...)
u.player.kick_ball() # or whatever you want to call
u.player.homeruns
u.referee.flag_play() # or whatever.
See if this works. I haven't actually tested it but it should be good.
The previous answer works other than :required => false is not recognized for has 1 properties.
It's also confusing because for has n properties, you can use new right on the property or otherwise treat it as a collection. In your example, you would be tempted to code
u = User.create ...
u.referee.create ...
But that fails in the case of has 1 because the property is a single value, which begins life as nil and so you have to use the method the previous answer indicates. Also, having to explicitly make the belongs_to association into the key is a little confusing.
It does seem to execute validations and have the right association actions (so u.save will also save the referred-to Referee). I just wish it were more consistent between has n and has 1.

Composite primary key is used only to access records but not save records in data_mapper

I have an app that tracks a player's progress over a fitness program. So every player has multiple weeks with the same :week_id
Week_id combined with the belongs_to relationship is the composite primary key for the Week record.
However, when I try to create two weeks with the same week_id that belong to different players, I get a "column week_id is not unique" error.
I feel like I am on the right track because when I want to fetch a week record, it tells me that I need two arguments to get it - the week_id and the player_id.
I am probably missing something simple here. I hope you can show me.
require "rubygems"
require "json"
require "sqlite3"
require "data_mapper"
require "bigdecimal"
DataMapper::setup(:default, "sqlite3://#{Dir.pwd}/prod.db")
DataMapper::Model.raise_on_save_failure = true
class Player
include DataMapper::Resource
property :name, String, :key => true
property :age, Integer
has n, :weeks
end
class Week
include DataMapper::Resource
property :week_id, Integer, :key => true
property :score, Integer
belongs_to :player, :key => true
end
DataMapper.finalize.auto_migrate!
#jack = Player.create(:name => "jack")
#jack.weeks.create(:week_id => 1)
#jill = Player.create(:name => "jill")
#jill.weeks.create(:week_id => 1)
Looks like you already came up with a solution, but I'll go ahead and post another answer for posterity, since I ran into the same issue.
It seems like declaring ':player_name' as a property that is a key as well as with the 'belongs_to' declaration, you end up with the same SQL to create the table, but it also recognizes that week_id is part of a composite key and does not need to be unique on its own. (It appears DataMapper has issues with composite keys split across 'property' and 'belongs_to' declarations.)
class Week
include DataMapper::Resource
property :week_id, Integer, :key => true
property :player_name, String, :key => true
property :score, Integer
belongs_to :player, :key => true
end
Results in the following SQL to create the table:
~ (0.000000) CREATE TABLE "weeks" ("week_id" INTEGER NOT NULL, "player_name" VARCHAR(50) NOT NULL, "score" INTEGER, PRIMARY KEY("week_id", "player_name"))
And your example code works without errors
So I ended up making a small change to my Week model like so
class Week
include DataMapper::Resource
property :id, String, :key => true
property :week_id, Integer
property :score, Integer
belongs_to :player
end
And whenever I create a new Player model I concatenate the foreign key and the week_id to create the id string
So my id string will look something like "jack1" for the first record for a player with an :id of "jack"
Not sure if this is the data_mapper way but it works.

Datamapper: How to count total score from has_many objects

I just started learning some database basics. I am using Ruby and the datamapper gem
I have two simple objects:
class Quote
include DataMapper::Resource
property :id, Serial
property :saying, String, :required => true
property :score, Integer, :default => 5
belongs_to :user
end
and
class User
include DataMapper::Resource
property :id, Serial
has n, :quotes
end
No I would like to get the total score of a user. The total score is the sum of the scores of all associated quotes of a user.
I tried something like
#totalscore = #user.quotes.inject(0) {|count, q| count + q.score}
but I guess this can't be the way I am supposed to use a database, right?
Any help is appreciated!
Best,
Tobi
I am not running the code, but by looking at the docs, I think something like this should work:
#totalscore = #user.quotes.sum :score

Chained aggregate call across association in DataMapper (ruby)

I am working on a simple budget app using Sinatra and DataMapper in Ruby.
I want to get the sum of all transactions across all income accounts within the last 30 days.
Something like Account.income_accounts.account_entries.sum(:amount, :transaction_date.gte => Date.today - 30) should work. Instead, the limiting condition on transaction_date is getting ignored, returning the sum of the amount for all entries for all income accounts.
Given the following:
class Account
include DataMapper::Resource
has n, :account_entries
property :id, Serial
property :name, String
property :acct_type, String
def self.income_accounts
all(:acct_type => 'Income')
end
end
class AccountEntry
include DataMapper::Resource
belongs_to :account
property :id, Serial
property :account_id, Integer
property :description, String
property :amount, BigDecimal
property :transaction_date, DateTime
end
I am properly requiring dm-aggregates. I am new to DataMapper. If it matters, I am using a sqlite3 database. I really don't want to resort to using ruby to sum the results. It also feels wrong to resort to executing raw SQL for this type of simple aggregate query.
Can anyone shed some light on this? I would love to be pointed in the right direction regarding chained finders in DataMapper, particularly with aggregates. My spelunking into the API and the DataMapper site hasn't yielded a solution as of yet.
I just wrote a small stand-alone script to test your example, and it appears to return the correct results. Please note I am using edge extlib, dm-core, and dm-more all installed from git:
#!/usr/bin/env ruby -Ku
# encoding: utf-8
require 'rubygems'
require 'dm-core'
require 'dm-aggregates'
DataMapper::Logger.new($stdout, :debug)
DataMapper.setup(:default, 'sqlite3::memory:')
class Account
include DataMapper::Resource
property :id, Serial
property :name, String
property :acct_type, String
has n, :account_entries
def self.income_accounts
all(:acct_type => 'Income')
end
end
class AccountEntry
include DataMapper::Resource
property :id, Serial
property :description, String
property :amount, BigDecimal
property :transaction_date, Date
belongs_to :account
end
DataMapper.auto_migrate!
account = Account.create(
:name => 'Test Account',
:acct_type => 'Income'
)
5.times do |n|
account.account_entries.create(
:description => "Account Entry #{n}",
:amount => 1.00,
:transaction_date => Date.today
)
end
puts Account.income_accounts.account_entries(:transaction_date.gte => Date.today - 30).sum(:amount).to_s('F') # => 5.0
Can you run the above program and let me know what it returns for you? If you get something other than 5.0, try updating to the latest packages and retry.
DateTime uses second as it's base unit Date.today - 30 is 30 seconds ago. Try Date.today - 30.days
Did you try DateTime.now-30 or maybe even Time.now-30*3600*24 instead of Date.today-30 ?
User error. I mucked around with to_s on DateTime to use the time formats in strftime. When removed, the chained aggregate worked as anticipated.

Resources