Sequel accessing many_to_many join table when adding association - ruby

I'm building a wishlist system using Sequel. I have a wishlists and items table and an items_wishlists join table (that name is what sequel chose). The items_wishlists table also has an extra column for a facebook id (so I can store opengraph actions), which is a NOT NULL column.
I also have Wishlist and Item models with the sequel many_to_many association set up. The Wishlist class also has the :select option of the many_to_many association set to select: [:items.*, :items_wishlists__facebook_action_id].
Is there a way that I can add in extra data when creating the association, like wishlist.add_item my_item, facebook_action_id: 'xxxx' or something? I can't do it after I create the association as the facebook id is has NOT NULL on the column.
Thanks for any help

The recommended way to do this is to add a model for the join table. However, if you don't want to do that, you can do:
class Wishlist
def _add_item(item, hash={})
model.db[:items_wishlists].insert(hash.merge(:item_id=>item.id, :wishlist_id=>id))
end
end

I think there is also another possibility.
First a MWE (minimal working example) of your question:
require 'sequel'
Sequel.extension :pretty_table #Sequel::PrettyTable.print()/Sequel::PrettyTable.string()
DB = Sequel.sqlite
DB.create_table(:wishlists){
primary_key :id
String :listname
}
DB.create_table(:items){
primary_key :id
String :descr
}
DB.create_table(:items_wishlists){
primary_key :id
foreign_key :wishlist_id, :wishlists
foreign_key :item_id, :items
add_column :facebook_id, type: :nvarchar
}
class Item < Sequel::Model
many_to_many :wishlists
end
class Wishlist < Sequel::Model
many_to_many :items
end
w1 = Wishlist.create(listname: 'list1')
w1.add_item(Item.create(descr: 'item 1'))
#w1.add_item(Item.create(descr: 'item 2'), facebook_id: 'fb2') ##<- This does not work
#Sequel::PrettyTable.print(Wishlist)
#Sequel::PrettyTable.print(Item)
Sequel::PrettyTable.print(DB[:items_wishlists])
To allow ad_itemwith a parameter (Wishlist#add_item(Item.create(descr: 'item 2'), facebook_id: 'fb2')) you must define an adder as in this example:
require 'sequel'
Sequel.extension :pretty_table
Sequel::PrettyTable.print()/Sequel::PrettyTable.string()
DB = Sequel.sqlite
DB.create_table(:wishlists){
primary_key :id
String :listname
}
DB.create_table(:items){
primary_key :id
String :descr
}
DB.create_table(:items_wishlists){
primary_key :id
foreign_key :wishlist_id, :wishlists
foreign_key :item_id, :items
add_column :facebook_id, type: :nvarchar
}
class Item < Sequel::Model
#~ many_to_many :wishlists
end
class Wishlist < Sequel::Model
many_to_many :items,
join_table: :items_wishlists, class: Item,
left_key: :wishlist_id, right_key: :item_id,
adder: (lambda do |item, facebook_id: nil|
self.db[:items_wishlists].insert(wishlist_id: self.id, item_id: item.id, facebook_id: facebook_id)
end)
end
w1 = Wishlist.create(listname: 'list1')
w1.add_item(Item.create(descr: 'item 1'))
w1.add_item(Item.create(descr: 'item 2'), facebook_id: 'fb2')
Sequel::PrettyTable.print(DB[:items_wishlists])
The result:
+-----------+--+-------+-----------+
|facebook_id|id|item_id|wishlist_id|
+-----------+--+-------+-----------+
| | 1| 1| 1|
|fb2 | 2| 2| 1|
+-----------+--+-------+-----------+
But the next problem will come later:
With w1.items you get the list of items, but you have no access to the parameters. (at least I found no way up to now. I'm still researching, but I expect, that I need a model of the join table for this (see Jeremys recommendation))

Related

I do not understand a many_to_one <=> one_to_one model association

how to say? I do not understand what the sequel documentation tries to tell me about associations in case of a two models linked over a foreign key in one model A being a primary key in the other in a may_to_one case.
I always thought: If it is many_to _one in one direction it has to be one_to_many in the other... but the sequel provides a confusing chapter meant to clarify the topic with in addition an example I cannot follow.
It says in
"Differences Between many_to_one and one_to_one"
If you want to setup a 1-1 relationship between two models, where the foreign > key in one table references the associated table directly, you have to use
many_to_one in one model, and one_to_one in the other model. How do
you know which to use in which model? The simplest way to remember is
that the model whose table has the foreign key uses many_to_one, and
the other model uses one_to_one"
And continues to provide this strange example:
# Database schema:
# artists albums
# :id <----\ :id
# :name \----- :artist_id
# :name
class Artist
one_to_one :album
end
class Album
many_to_one :artist
end
In albums I may find several rows pointing to same artist... why shouldn't the artist point back to all his/her albums?
The sequel docu is crazy hard to read in many cases but this chapter reads easy but makes no sense for me:(
Same issue for me.
require "logger"
require "sequel"
db = Sequel.connect "postgres://localhost/postgres", :logger => Logger.new(STDOUT)
db.drop_table :artists, :cascade => true if db.table_exists?(:artists)
db.create_table :artists do
primary_key :id
foreign_key :album_id, :albums
end
db.drop_table :albums, :cascade => true if db.table_exists?(:albums)
db.create_table :albums do
primary_key :id
foreign_key :artist_id, :artists
end
class Artist < Sequel::Model(db[:artists])
one_to_one :album
end
class Album < Sequel::Model(db[:albums])
one_to_one :artist
end
artist_1 = Artist.create
album_1 = Album.create
artist_1.update :album => album_1
album_1.reload
puts album_1.artist.nil?
artist_2 = Artist.create
album_2 = Album.create
album_2.update :artist => artist_2
artist_2.reload
puts artist_2.album.nil?
We can fix this example by replacing any of one_to_one into many_to_one.
class Album
many_to_one :artist
end
In this case artist.album_id won't be used.
class Artist
many_to_one :albums
end
In this case album.artist_id won't be used.
The problem is that method names one_to_one and many_to_one were selected by underlying sequel logic and they are not user friendly.
You can create user friendly aliases for these methods. I prefer just to use it with comments. For example:
db.create_table :artists do
primary_key :id
foreign_key :album_id, :albums
end
db.create_table :albums do
primary_key :id
end
class Artist < Sequel::Model(db[:artists])
many_to_one :album # I have album_id foreign key
end
class Album < Sequel::Model(db[:albums])
one_to_one :artist # I don't have artist_id foreign key
end

Data in a many-to-many relationship

I want to store extra data in a many-to-many relationship, similar to this Django example, in Sequel and Postgres.
In my example, each person has a place they work, and what hours they work there. Each person can work different hours at the same place. For example...
Joe works at The Bar Mon, Wed & Fri 9-5pm.
Joe also works at The Bar Saturday noon-midnight.
Joe works at Some Pub Sunday 12-5.
Joe started working at The Bar 2013-12-07
Joe started working at Some Pub 2014-12-23
I'm wondering how to set up the tables and models to make this as not awkward as possible. Currently I have a schema like this.
create_table(:people) do
primary_key :id
String :name
end
create_table(:works) do
primary_key :id
String :name
end
create_table(:employments) do
primary_key :id
foreign_key :work_id, :work, null: false,
foreign_key :person_id, :people, null: false,
Date :started
end
create_table(:hours) do
primary_key :id
foreign_key :employment_id, :employments, null: false
String :dow, null: false,
Time :start, null: false, only_time: true
Time :end, null: false, only_time: true
end
How should I create the model associations to join it all together? Ideally I would like to be able to say something like...
joes_hours_at_some_pub = joe.employment("Some Pub").hours
...without having to go through the intermediate join table.
Sequel's advanced association doc has an example of this. Define an Employment model and use :join_table to create a relationship through it.
class Person < Sequel::Model
one_to_many :employments,
many_to_many :works, join_table: :employments
# This allows joe.employment(work).hours
def employment(work)
employments_dataset.where(work_id: work.id)
end
end
class Work < Sequel::Model
one_to_many :employments,
many_to_many :people, join_table: :employments
end
class Employment < Sequel::Model
many_to_one :person
many_to_one :work
one_to_many :hours
end
class Hours < Sequel::Model
many_to_one :practice
end

Filter based on join table's fields in Sequel many_to_many relationship

Assuming:
a many_to_many association defined in the many_to_many section of the Sequel documentation; artists --< albums_artists >-- albums
albums_artists has a foo field
How do I access the fields on the albums_artists table in a WHERE clause? This syntax doesn't work:
<% #artist.albums_dataset.where(:foo => 'bar').each do |album| %>
...
<%= album[:foo] %>
...
<% end %>
If I were to add a model for album_artists, creating two associations in the process, would I remove the existing many_to_many association?
You need to qualify the table name for the column you want to access, using the syntax :tablename__columnname (two underscores).
#artist.albums_dataset.filter(:albums_artists__foo => "bar").each do |album|
Here's a test, also showing the generated SQL:
require 'sequel'; DB = Sequel.sqlite
DB.create_table(:artists){ primary_key :id; String :name }
DB.create_table(:albums){ primary_key :id; String :name }
DB.create_table(:albums_artists) do
String :foo
foreign_key :artist_id, :artists
foreign_key :album_id, :albums
primary_key [:artist_id, :album_id]
end
class Artist < Sequel::Model; many_to_many :albums; end
class Album < Sequel::Model; many_to_many :artists; end
Artist.multi_insert [ {name:"Bob"}, {name:"Dylan"}, {name:"Joni"} ]
Album.multi_insert [ {name:"BobSolo"}, {name:"JoniSolo"},
{name:"BobJoni"}, {name:"BobDylan"},
{name:"BobJoniDylan"} ]
# If the album name includes the artist, add them to it.
Artist.each do |ar| Album.each do |al|
if al.name.include?(ar.name)
DB[:albums_artists] << {artist_id:ar.id, album_id:al.id}
end
end end
bob.albums.map(&:name)
#=> ["BobSolo", "BobJoni", "BobDylan", "BobJoniDylan"]
bob.albums_dataset.filter(albums_artists__foo:"bar").sql
#=> SELECT `albums`.*
#=> FROM `albums` INNER JOIN `albums_artists`
#=> ON (`albums_artists`.`album_id` = `albums`.`id`)
#=> WHERE ((`albums_artists`.`artist_id` = 1)
#=> AND (`albums_artists`.`foo` = 'bar'))
bob.albums_dataset.filter(albums_artists__foo:"bar").all
#=> [#<Album #values={:id=>5, :name=>"BobJoniDylan"}>]

SQLite Creating Table Dynamically

I have a table in SQLite and when an entry is made in this table, I would like to create a new table for each entry in the first table. (I want to do this in Ruby On Rails, if that helps)
Here is an example to clarify what I am trying to achieve:
Assume there is a table: Campaigns
Campaigns
Campaign_ID, date, name
So if I make an entry:
01 06/12 FirstCampaign
is there a way to create a new table called: {Campaign_ID}_Page
i.e:
01_Page
(fields for this table go here)
Why do you need it?
I think it would be better to create a table Pages with a foreign key to your table Campaigns.
An example (I use Sequel):
require 'sequel'
DB = Sequel.sqlite
DB.create_table :Campaigns do
primary_key :id
column :campaign_id, :integer
column :date, :date
column :name, :string
end
DB.create_table :Pages do
primary_key :id
foreign_key :campaign_id, :Campaigns
column :text, :string
end
key = DB[:Campaigns].insert(:campaign_id => 01, :date=> Date.new(2012,1,1), :name => 'FirstCampaign')
DB[:Pages].insert(:campaign_id => key, :text => 'text for FirstCampaign')
key = DB[:Campaigns].insert(:campaign_id => 02, :date=> Date.new(2012,1,1), :name => 'SecondCampaign')
DB[:Pages].insert(:campaign_id => key, :text => 'text for SecndCampaign')
#All pages for 1st campaign
p DB[:Pages].filter(
:campaign_id => DB[:Campaigns].filter(:campaign_id => 1).first[:id]
).all
But to answer your question: You could try to use a model hook.
An example with Sequel:
require 'sequel'
DB = Sequel.sqlite
DB.create_table :Campaigns do
primary_key :id
column :campaign_id, :integer
column :date, :date
column :name, :string
end
class Campaign < Sequel::Model
def after_create
tabname = ("%05i_page" % self.campaign_id).to_sym
puts "Create table #{tabname}"
self.db.create_table( tabname ) do
foreign_key :campaign
end
end
end
p DB.table_exists?(:'01_page') #-> false, table does not exist
Campaign.create(:campaign_id => 01, :date=> Date.new(2012,1,1), :name => 'FirstCampaign')
p DB.table_exists?(:'00001_page') #-> true Table created
My example has no test, if the table already exist. If you really want to use it,

How to save newly-created associated models using Sequel?

Assuming I have something like this:
# Schema:
DB.create_table :transactions do
primary_key :id
foreign_key :card_id, :cards
Integer :amount
end
DB.create_table :cards do
primary_key :id
foreign_key :transaction_id, :transactions
Intger :number
end
# Models:
class Transaction < Sequel::Model
one_to_one :card
end
class Card < Sequel::Model
one_to_one :transaction
end
How do I make this work, such that it saves both trans, card, and their respective associations?
trans = Transaction.new(:amount => 100)
card = Card.new(:number => 4000500060007000)
trans.card = card
trans.save
As it stands, this doesn't work because card isn't saved first, and Sequel throws a "no primary key" error. If I save the card first, it won't get the transaction's id.
Basically, I'm trying to avoid this:
# Save without associations first, but this will assign primary keys
trans.save
card.save
# Now, manually create associations
trans.card = card
card.trans = trans
# Re-save again, this time with associations
trans.save
card.save
You may want to try and change the association type to something more like:
# Schema:
DB.create_table :transactions do
primary_key :id
Integer :amount
end
DB.create_table :cards do
primary_key :id
foreign_key :transaction_id, :transactions
Integer :number
end
# Models:
class Transaction < Sequel::Model
one_to_many :card
end
class Card < Sequel::Model
one_to_one :transaction
end
Now you create as:
trans = Transaction.new(:amount => 100)
trans.save
trans.add_card(:number => 4000500060007000)
This will allow for all the same options as well as permitting (but certainly not requiring) a transaction to be split across multiple cards.

Resources