How to get data from other tables using Sequel - ruby

I use Sequel and I have the following 2 models in my Sinatra app
# foo.rb
require 'sequel'
module Notes
module Models
class Foo < Sequel::Model
plugin :json_serializer, naked: true
plugin :validation_helpers
one_to_many :bars
def validate
super
validates_presence [:foo_name], message: "can't be empty"
end
end
end
end
# bar.rb
require 'sequel'
module Notes
module Models
class Bar < Sequel::Model
plugin :json_serializer, naked: true
plugin :validation_helpers
many_to_one :foo
def validate
super
validates_presence [:bar_name], message: "can't be empty"
end
end
end
end
There is a foo_id foreign key in the Bar table.
I have an API route where you can get all bars or just a specific bar if a parameter is passed in, looks something like:
app.get '/api/bars' do
require_session
bar_name = params[:bar_name]
bar_model = Models::Bar
if bar_name.nil?
bar_model.all.to_json
else
bar_model.where(Sequel.ilike(:bar_name, '%' + bar_name + '%'))
.all
.to_json
end
end
What I'd like to do but haven't figured out yet is how can I also get at least foo_name from the Foo table in the result as well, retrieved based on the foo_id that's in Bar table?
Even more, what if there are even longer association, say there's another foreign key, for example baz_id, in the Foo table that's linked to a Baz table, and in this same API, I also want to get all the info from the Foo and Baz tables based on the foreign key association in those respective tables.
Hope that makes sense and any help is greatly appreciated.

You can use the :include option of Sequel's to_json method to do this. You can specify this in the model when you set the json_serializer options, but you can also override those defaults when you call to_json on an instance.
So, on an individual instance of Bar, you could do:
Bar.first.to_json(include: {foo: {only: :foo_name}})
If you want to do it for a full list, as in your example, you would want to call it on the class. Note that if you call it on an array this won't work, so you must call it on the dataset before converting it to an array.
# all models
Bar.to_json(include: {foo: {only: :foo_name}})
# a subset of models
Bar.where(Sequel.ilike(:bar_name, '%' + bar_name + '%'))
.to_json(include: {foo: {only: :foo_name}})

Related

alias_attribute vs read_attribute and write_attribute

I'm using ActiveRecord without rails. Everything works fine except for a weird quirk with some helper methods I'm writing that I'm hoping someone can explain. I've got a model of a legacy database. Some columns names have a "#" in them so I defined them in the model using read_attribute and write_attribute. For example (accurate example but simplified):
class Product < ActiveRecord::Base
alias_attribute :name, :pname
def sale_number
read_attribute 'sale#'
end
def sale_number=(value)
write_attribute 'sale#', value
end
def self.helper_with_alias
where(name: 'My Product Name')
end
def self.helper_with_attribute
where(sale_number: 5)
end
end
If I call Product.helper_with_alias everything works as expected. But when I call Product.helper_with_attribute I get a ActiveRecord::StatementInvalid error saying that the column sale_number could not be found. In addition, if I replace the code in helper_with_attribute to where('sale#' => 5) everything works fine.
Why does ActiveRecord correctly alias pname to name but not correctly alias sale# to sale_number?
The error is because you are using an in-existent column in the where clause. You also need to define alias_attribute for sale# to sale_number.
In your model, you can do:
class Product < ActiveRecord::Base
alias_attribute :sale_number, :"sale#"
def self.helper_with_attribute
where(sale_number: 5)
end
end
With this you don't need to define setters and getters just for assignment and retrieval purposes, so you can remove sale_number and sale_number=(value) methods. With alias_attribute, getters, setters and query methods are already aliased!
Why does ActiveRecord correctly alias pname to name but not correctly
alias sale# to sale_number?
This is because you have defined alias_attribute :name, :pname which provided the setters, getters and query methods as alias to your existing pname attribute. But, for sale_number, you've only defined a getter and setter but not the query methods.

Make friendly_id scope play nice with subclassed ActiveRecord model

I have a subclassed ActiveRecord model which uses a separate table to store records and friendly_id (4.1.0.beta.1) to generate slugs. Problem is friendly_id is using the parent class's table to check for existing slugs, instead of using the child table. Basically I'd like friendly_id to scope its checks to the right table.
Example:
class Parent
friendly_id :name, :use => :slugged
end
class Child < Parent
self.table_name = 'children'
end
Parent.create(name: 'hello').slug
> 'hello'
Child.create(name: 'hello').slug
> 'hello--2'
I want friendly_id to generate the 'hello' slug for the second create, because there are no records in the children table with that slug. Is there a way to configure or monkey patch the class friendly id uses for its queries?
EDIT: added friendly_id version for future reference
I'm posting my own solution to this problem, just in case someone is having the same problem. I should reiterate that this problem was found on version 4.1.0.beta.1 of the friendly_id gem (which at the time was the most recent version), so this issue may not occur any more.
To solve this problem, I basically configured slug_generator_class to use my own class, so I could monkey patch the culprit method.
In my model:
friendly_id do |config|
config.slug_generator_class = SubclassScopableSlugGenerator
end
In an initializer, I overrode the FriendlyId::SlugGenerator.conflicts method so I could access the sluggable_class var:
# Lets a non-STI subclass of a FriendlyId parent (i.e. a subclass with its
# own dedicated table) have independent slug uniqueness.
class SubclassScopableSlugGenerator < FriendlyId::SlugGenerator
private
def conflicts
# this is the only line we're actually changing
sluggable_class = friendly_id_config.model_class
pkey = sluggable_class.primary_key
value = sluggable.send pkey
base = "#{column} = ? OR #{column} LIKE ?"
# Awful hack for SQLite3, which does not pick up '\' as the escape character without this.
base << "ESCAPE '\\'" if sluggable.connection.adapter_name =~ /sqlite/i
scope = sluggable_class.unscoped.where(base, normalized, wildcard)
scope = scope.where("#{pkey} <> ?", value) unless sluggable.new_record?
length_command = "LENGTH"
length_command = "LEN" if sluggable.connection.adapter_name =~ /sqlserver/i
scope = scope.order("#{length_command}(#{column}) DESC, #{column} DESC")
end
end

data_mapper, attr_accessor, & serialization only serializing properties not attr_accessor attributes

I'm using data_mapper/sinatra and trying to create some attributes with attr_accessor. The following example code:
require 'json'
class Person
include DataMapper::Resource
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json
produces this output:
"{\"id\":null,\"first_name\":\"Mike\"}"
Obviously I would like for it to give me both the first and last name attributes. Any ideas on how to get this to work in the way one would expect so that my json has all of the attributes?
Also, feel free to also explain why my expectation (that I'd get all of the attributes) is incorrect. I'm guessing some internal list of attributes isn't getting the attr_accessor instance variables added to it or something. But even so, why?
Datamapper has it’s own serialization library, dm-serializer, that provides a to_json method for any Datamapper resource. If you require Datamapper with require 'data_mapper' in your code, you are using the data_mapper meta-gem that requires dm-serializer as part of it’s set up.
The to_json method provided by dm-serializer only serializes the Datamapper properties of your object (i.e. those you’ve specified with property) and not the “normal” properties (that you’ve defined with attr_accessor). This is why you get id and first_name but not last_name.
In order to avoid using dm-serializer you need to explicitly require those libraries you need, rather than rely on data_mapper. You will need at least dm-core and maybe others.
The “normal” json library doesn’t include any attributes in the default to_json call on an object, it just uses the objects to_s method. So in this case, if you replace require 'data_mapper' with require 'dm-core', you will get something like "\"#<Person:0x000001013a0320>\"".
To create json representations of your own objects you need to create your own to_json method. A simple example would be to just hard code the attributes you want in the json:
def to_json
{:id => id, :first_name => first_name, :last_name => last_name}.to_json
end
You could create a method that looks at the attributes and properties of the object and create the appropriate json from that instead of hardcoding them this way.
Note that if you create your own to_json method you could still call require 'data_mapper', your to_json will replace the one provided by dm-serializer. In fact dm-serializer also adds an as_json method that you could use to create the combined to_json method, e.g.:
def to_json
as_json.merge({:last_name => last_name}).to_json
end
Thanks to Matt I did some digging and found the :method param for dm-serializer's to_json method. Their to_json method was pretty decent and was basically just a wrapper for an as_json helper method so I overwrote it by just adding a few lines:
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
The completed method override looks like:
module DataMapper
module Serializer
def to_json(*args)
options = args.first
options = {} unless options.kind_of?(Hash)
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
result = as_json(options)
# default to making JSON
if options.fetch(:to_json, true)
MultiJson.dump(result)
else
result
end
end
end
end
This works along with an attributes method I added to a base module I use with my models. The relevant section is below:
module Base
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def attr_accessor(*vars)
#attributes ||= []
#attributes.concat vars
super(*vars)
end
def attributes
#attributes || []
end
end
def attributes
self.class.attributes
end
end
now my original example:
require 'json'
class Person
include DataMapper::Resource
include Base
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json :include_attributes => true
Works as expected, with the new option parameter.
What I could have done to selectively get the attributes I wanted without having to do the extra work was to just pass the attribute names into the :methods param.
p ps.to_json :methods => [:last_name]
Or, since I already had my Base class:
p ps.to_json :methods => Person.attributes
Now I just need to figure out how I want to support collections.

does a sequel models generator exists?

I'm looking for a ruby class that could generate the sequel model file for Ramaze after reading the definition of the table in a mySQL database.
For example, I would like to type :
ruby mySuperGenerator.rb "mytable"
And the result shold be the file "mytable.rb" in "model" directory, containing :
class Mytable < Sequel::Model(:mytable)
# All plugins I've defined somewhere before lauching the generator
plugin :validation_helpers
plugin :json_serializer
one_to_many :othertable
many_to_one :othertable2
def validate
# Generating this if there are some not null attributes in this table
validates_presence [:fieldthatshoulnotbenull1, :fieldthatshoulnotbenull2]
errors.add(:fieldthatshoulnotbenull1, 'The field fieldthatshoulnotbenull1 should not be null.') if self.fieldthatshoulnotbenull1.nil?
end
def before_create
# All the default values found for each table attributes
self.creation_time ||= Time.now
end
def before_destroy
# referential integrity
self.othertable_dataset.destroy unless self.othertable.nil?
end
end
Does someone knows if such a generator exists ?
Well...
I finally wrote my script.
see https://github.com/Pilooz/sequel_model_generator Have look and fork !

Overriding methods in ActiveRecord::QueryMethods

I want to be able to override certain methods in ActiveRecord::QueryMethods for educational and experimental reasons.
Example: User is an ActiveRecord class that includes modules that overwrite the QueryMethod "order":
User.where("last_logged_in_at < ?", 1.year.ago).order("my own kind of arguments here")
However, I can't seem to get things to work. What module should I override? Something in the ARel gem, AR::Relation, or AR::QueryMethods?
I think the answer is to track down where the existing Arel order is defined.
module ActiveRecord
module QueryMethods
def order(*args)
relation = clone
relation.order_values += args.flatten unless args.blank?
relation
end
end
end
A quick test in console verifies change this will work
module ActiveRecord::QueryMethods
def order(*args)
relation = clone
if args.first
puts "ordering in ascending id"
relation.order_values += ["id ASC"]
else
puts "ordering in descending id"
relation.order_values += ["id DESC"]
end
relation
end
end
So, you can do something like this.
But my suggestion would be to create a custom my_order which keeps the original order intact, but encapsulates the same logic.
But you can define this straight on active record
class ActiveRecord::Base
class << self
def my_order(*args)
self.order(*my logic for ordering*)
end
end
end

Resources