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

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.

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.

Datamapper - weird may not be NULL error

I'm using DataMapper in a simple application to track sales. I have a Day class, like this:
class Day
include DataMapper::Resource
property :id, Serial, :key => true
property :date, DateTime
property :bestseller, String
property :total_money, Decimal
property :total_sold, Integer
property :total_orders, Integer
has n, :sales
end
and a Sales class:
class Sale
include DataMapper::Resource
belongs_to :day
property :id, Serial, :key => true
property :name, String
property :amount, Integer
property :value, Integer
end
When trying to add a new Sale to the database, like so:
s = Sale.new(:day => Day.get(1), :name => "Foo", :amount => "42", :value => "42"
I get this error when calling save.
DataObjects::IntegrityError at /sell
sales.date may not be NULL
I have no date property in Sale, so I'm not sure where this is coming from. First I thought that the Day object I'm getting doesn't have a day set, so I did d = Day.get(1).date = Time.now and saved it, but this doesn't resolve the error.
What did I break?
EDIT The sqlite3 schema
CREATE TABLE "sales" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" VARCHAR(50),
"amount" INTEGER,
"value" INTEGER,
"day_id" INTEGER NOT NULL,
"drink_id" INTEGER NOT NULL
);
CREATE INDEX "index_sales_day" ON "sales" ("day_id");
CREATE INDEX "index_sales_drink" ON "sales" ("drink_id");
I think I fixed it. Apparently, I had an old date property at one point in Sale. I entered the ruby interpreter, required my model and used DataMapper.auto_migrate! to reset the entire database. This fixed the problem.

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.

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.

datamapper multi-field unique index

In Datamapper, how would one specify the the combination of two fields must be unique. For example categories must have unique names within a domain:
class Category
include DataMapper.resource
property :name, String, :index=>true #must be unique for a given domain
belongs_to :domain
end
You have to create a unique index for the two properties:
class Category
include DataMapper::Resource
property :name, String, :unique_index => :u
property :domain_id, Integer, :unique_index => :u
belongs_to :domain
end
Actually, John, Joschi's answer is correct: the use of named :unique_index values does create a multiple-column index; it's important to read the right-hand side of those hash-rockets (i.e., if it had just been true, you would be right).
Did you try to define both properties as keys? Not sure I have tried it but that way they should become a composite key.
property :name, String, :key => true
property :category, Integer, :key => true

Resources