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
Related
Using the ruby sequel gem, I have a small (sqlite3) database with the following schema:
db.create_table :item_types do
primary_key :id
String :name, unique: true, null: false
end
db.create_table :item_values do
primary_key :id
String :name, unique: true, null: false
end
db.create_table :items do
primary_key :id
foreign_key :type, :item_types, key: :id
foreign_key :value, :item_values, key: :id
String :name, unique: true, null: false
String :desc, null: false
end
Ideally, I'd like to be able to be able to use similar to
item = Item.first(...)
puts item.type.name, item.value.name
Reading through the association guide, it looks like I need a one_to_one association in the Item model for item_types and item_values. However, I'm not sure of the proper way to link them. Should the foreign key be passed via the key option, or is there a more appropriate method?
The guide illustrates setting up a one to many association with this example:
# Database schema:
# artists albums
# :id <----\ :id
# :name \----- :artist_id
# :name
class Artist < Sequel::Model
one_to_many :albums
end
class Album < Sequel::Model
many_to_one :artist
end
To be honest I have never used Sequel but from guide it seems like you should be doing something like:
class ItemType < Sequel::Model
one_to_many :items
end
class ItemValue < Sequel::Model
one_to_many :items
end
class Item < Sequel::Model
many_to_one :type, class: 'ItemType', key: 'item_type_id'
many_to_one :value, class: 'ItemValue', key: 'item_value_id'
end
Congratulations. You just ported the EAV anti-pattern to Sequel.
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
I'm writing a forum system (in Ruby, using Sequel), and one of the requirements is for users to be able to "star" a thread, which is vaguely equivalent to "subscription" features most forums support. I'm unsure about how to store the starring in the database, and especially on how to query for starred/unstarred threads for a given user, or checking whether a thread is starred.
Any tips would be greatly appreciated, and if you happen to know your way around Sequel, an example model would be absolutely grand.
This is very simple to implement:
First your migration:
create_table(:subscriptions, ignore_index_errors: true) do
primary_key :id
column :created_at, 'timestamp with time zone'
foreign_key :user_id, :users, null: false, key: [:id], index: true, on_delete: :cascade
foreign_key :thread_id, :threads, null: false, key: [:id], index: true, on_delete: :cascade
end
Your Models:
app/models/subscription.rb
class Subscription < Sequel::Model
many_to_one :user
many_to_one :thread
end
app/models/user.rb
class User < Sequel::Model
one_to_many :subscriptions
many_to_many :subscribed_threads,
class: :Thread,
join_table: :subscriptions,
left_key: :user_id,
right_key: :thread_id
end
app/models/thread.rb
class Thread < Sequel::Model
one_to_many :subscriptions
many_to_many :subscribers,
class: :User,
join_table: :subscriptions,
left_key: :thread_id,
right_key: :user_id
end
Query as follows
# all threads a user is subscribed to
user.subscribed_threads
# all subscribers to a thread
thread.subscribers
# all subscriptions to a thread in the last 3 days
thread.subscriptions.where{created_at >= Date.today - 3}
I suggest also configuring the dataset associations plugin on all your models:
# Make all model subclasses create association methods for datasets
Sequel::Model.plugin :dataset_associations
You can then compose and chain queries through associations with conditions more conveniently:
# all new subscribers for a thread in the last 3 days who are moderators
thread.subscriptions.where{created_at >= Date.today - 3}.user.where(moderator: true)
There are some powerful filtering and querying possibilities:
http://sequel.jeremyevans.net/rdoc/files/doc/dataset_filtering_rdoc.html
http://sequel.jeremyevans.net/rdoc/files/doc/querying_rdoc.html
I am having issues constructing the proper models, associations, and query for the following scenario and then returning results as JSON using Sequel with Ruby.
The database structure___
You can create a list of books. Each library contains books. Defined by the following:
db.create_table(:books) do
primary_key :id
String :name
String :author
DateTime :created
end
db.create_table(:libraries) do
primary_key :id
String :name
String :city
String :state
DateTime :created
end
db.create_table(:libraries_books) do
Integer :library_id
Integer :book_id
primary_key [:library_id, :book_id]
end
class Library < Sequel::Model(:libraries)
many_to_many :libraries_books, :left_key=>:library_id, :right_key=>:book_id, :join_table=>:libraries_books
one_to_many :libraries_books, :key=>:library_id
end
class LibraryBook < Sequel::Model(:libraries_books)
many_to_one :libraries
many_to_one :books
end
I am trying to determine the correct way to access all the book names for a given library. I initially tried to follow the Sequel Associations guide but was not able to figure out how I could use LibraryBook with associations to get all the books for a library and join on the Book model to get the proper columns.
After getting stuck with some of the methods described, I attempted to create my own query as such:
LibraryBook.select(:books.*)
.join_table(:inner, :libraries, :id => :library_id)
.join_table(:inner, :books, :id => :book_id)
.where(:library_id => 1)
Which seems to get me partially there. However, when I use the serialization extension, I get an error when the results are being converted:
undefined method `book_id' for #<LibraryGame:0x007fa9e904b470>
Any insight into that can be provided would be very helpful!
Try the following:
db.create_table(:books) do
primary_key :id
String :name
String :author
DateTime :created
end
db.create_table(:libraries) do
primary_key :id
String :name
String :city
String :state
DateTime :created
end
db.create_table(:books_libraries) do
foreign_key :library_id, :libraries, key: :id
foreign_key :book_id, :books, key: :id, index: true
primary_key [:library_id, :book_id]
end
class Library < Sequel::Model
many_to_many :books
end
class Book < Sequel::Model
many_to_many :libraries
end
Note renaming the libraries_books table to books_libraries and the use of the foreign_key directive for referential integrity. Conventions should allow things to just work.
Library[7].books # returns all books for library '7'
Or alternatively:
Book.where(libraries: Library[7])
Or multiple libraries:
Book.where(libraries: Library.where(id: [3,7,9]))
If sequel is not able to do the inflection for Library/Libraries then you may need to add your own inflection rule, eg:
Sequel.inflections do |inflect|
inflect.irregular 'Library', 'Libraries'
end
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.