Fetching values via foreign keys with DataMapper - ruby

I have two tables, nodes and terms.
The relevant fields in nodes are: nid (primary key) and value
In terms, they are: value, tid, and nid, where value and tid together are the primary key and nid is a foreign key referencing nodes.nid.
I want to add records to terms. I have the tid and nid, and the value I want to pull from the corresponding node - e.g. look up the value for a given nid in node and then put that as the value in terms.
A way to do this in SQL might be:
INSERT INTO terms(tid, nid, value)
values(mytid, mynid, (
select value from nodes where nid=mynid
));
Could somebody help me do this with DataMapper?
class Node
include DataMapper::Resource
property :nid, Serial, :key => true
property :value, Integer
end
class Term
include DataMapper::Resource
property :tid, Integer, :key => true
# how do I define nid and value?
end
# and then what do I give to Term.new or Term.create and how?
If anyone could point me to a good tutorial of DataMapper as well, I'd appreciate it. I've been using their online docs, but I've found the situations I find myself in are rarely covered there.

From your description the models you're looking for should be setup like that:
class Node
include DataMapper::Resource
property :nid, Serial
property :value, Integer
end
class Term
include DataMapper::Resource
property :tid, Integer, :key => true
property :value, Integer, :key => true
belongs_to :node, :child_key => :nid
end
You can work with these models like that:
# create a node
node = Node.create(:value => 123)
# create a term and associate it with the node
term = Term.create(:tid => 321, :node => node, :value => node.value)

Related

Ruby Sinatra Datamapper/database confusion

I'm confused as to how database relationship works.
Say I have a Border Crossing('crossing'), which has two
Directions('north', 'south'), each of which direction has 2 types of lanes ('normal','fast'), each of which lane has 2 metrics (=data) ('delay','queue_length').
In reality there are several crossings, with more lane types and more metrics.
How the heck should I store that in a database? I've used databases before, but never did table joins or one-to-many or anything like that.
I came across Datamapper and since I'm learning how to us Sinatra I thought I'd give it a go.
In the tutorial (http://datamapper.org/getting-started.html), the "one-to-many" part just screamed "this is what you need", so I started fiddling around.
require 'data_mapper'
DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/development.db")
class Crossing
include DataMapper::Resource
property :id, Serial
property :name, String,:unique=>true
has n, :directions
end
class Direction
include DataMapper::Resource
property :id, Serial
property :direction, String,:unique=>true
belongs_to :crossing
has n, :lanes
end
class Lane
include DataMapper::Resource
property :id, Serial
property :lane, String
belongs_to :direction
has n, :datas
end
class Data
include DataMapper::Resource
property :id, Serial
property :name, String,:unique=>true
property :value, String
belongs_to :lane
end
DataMapper.finalize.auto_migrate!
I just thought this looked so elegantly put: "crossing has n directions, directions has n lanes, etc"
Then:
Crossing.create(:name => "crossing")
Direction.create(:direction => "north")
Direction.create(:direction => "south")
Lane.create(:lane => 'normal')
Lane.create(:lane => 'fast')
Data.create(:data => 'delay')
Data.create(:data => 'queue_length')
// now how do I retrieve find the data of a lane of a direction of a crossing?
Now, what I will input and retrieve all the time is the Data part. Does this whole thing make sense or I'm just not understanding what table associations are for? I know I could just have a gigantic object instead of this but I'm pretty sure that's a weird way of doing things.
#crossing = {
'crossing name' => {
:directions => {
:north => {
:normal => {
:delay => '10 min',
:queue => '100 m'
},
:fast => {
:delay => '1 min',
:queue => '10 m'
}
},
etc etc etc
}
and then access the data like #crossing[:north][:normal][:delay]....but I kinda feel like a database would be better?
Am I making any sense in any way? Anybody got some pointers for a young grasshoper?
I would rather go with this structure:
Data belongs to Crossing, Direction and Lane; it has properties for delay and queue
Direction has many Data, and has exactly two rows
Lane has many Data, and has exactly two rows
Crossing has many Data, and has many rows
The reason is, you don't want to repeat the strings "north", "south" etc in your database.
Then, first seed the database with constant tables:
Direction.create(direction: 'north')
Direction.create(direction: 'south')
Lane.create(lane: 'normal')
Lane.create(lane: 'fast')
Then you can make your crossings:
cool_crossing = Crossing.create(name: 'My Cool Crossing')
not_cool_crossing = Lane.create(name: 'My Not So Cool Crossing')
and add data points:
north = Direction.first(name: "north")
normal = Lane.first(name: "normal")
Data.create(
crossing: cool_crossing,
lane: normal,
direction: north,
delay: 10,
queue: 1
)
and retrieve data by:
all_data_for_cool_crossing = Data.all(
crossing: cool_crossing
)
or
data_for_cool_crossing_normal_north = Data.first(
crossing: cool_crossing,
lane: normal,
direction: north
)

How to add a uuid primary key to existing table without id column

Other than dropping and recreating a table, how can I add a primary key id column of type uuid to a table that was created without an id field - a join table?
I tried:
add_column :organizations_users, :id, :uuid, primary_key: true
but the column doesn't get set as a primary key.
Looking at the new_column_definition ActiveRecord method, the code above should work:
column.primary_key = type == :primary_key || options[:primary_key]
I must be having a brain fart...
An old question, but if anyone comes across it, this worked for me in Rails 5.2. I was already using UUIDs for primary key :id columns in other tables, so you might need to set that up first if you're not.
def change
add_column :products_questions, :id, :uuid, primary_key: true, default: -> { "gen_random_uuid()" }
end

How to check constraints in Sequel

How do I create a CHECK constraint to check for a range of possible values in Sequel. a Ruby ORM.
All attempts seem to generate CHECK (1 = 0) as seen in the output logs.
Here's the table I'm trying to model using Sequel's DSL:
create table memberships(
id integer primary key autoincrement
, group_id integer references groups(id) on delete cascade
, user_id integer references users(id) on delete cascade
, role char check (role in ('u','a','o')) default 'u'
, unique(group_id, user_id, role)
);
and here's the Sequel schema generation code:
db.create_table(:memberships){
primary_key :id
foreign_key :user_id, :users
foreign_key :group_id, :groups
char :role, default: 'u'
check{role=='u' or role=='a'} #<-----this line generates CHECK (1 = 0)
unique [:user_id, :group_id, :role]
}
In Sequel, check constraints are handled just like a filter expression. The recommended way to handle your case would be:
check(:role=>%w[a o u])
I agree that more documentation would probably be better, though there are examples in http://sequel.jeremyevans.net/rdoc/files/doc/schema_modification_rdoc.html
The documentation on how check and add_constraint are supposed to work is rather sparse but you could try bypassing all the magic entirely, write the constraint as you would in SQL, and return that from the block; something like this:
db.create_table(:memberships) {
#...
check { "role in ('a', 'o', 'u')" }
#...
}
The language that is allowed in a CHECK constraint is quite rich and varied so I'd expect a simple string to be an option.
This would be handled elegantly with an enum:
up do
extension :pg_enum
create_enum(:role_types, %w[a b c])
create_table # ...
role_types :role, null: false
#...
down do
drop_table # :...
drop_enum :role_types
end

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.

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

Resources