ActiveAdmin form for nested jsonb field - ruby-on-rails

I am building a new/edit form for an ActiveRecord model in ActiveAdmin. One of the attributes of that model is a jsonb attribute. Its value is expected to be a Hash whose values are themselves Hashes:
{
'foo' => {
'a' => 1,
'b' => 2
},
'bar' => {
'c' => 3,
'd' => 4
}
}
The MVP that I'm going for is to generate a form that looks like this:
Foo
a __________________
b __________________
Bar
c __________________
d __________________
So essentially, I only want to ask the user to input values for each of the inner Hashes. I've been having some trouble doing this with ActiveAdmin. This is my best attempt to render the above form, but it only renders the "Bar" section. :jsonb_field is the name of the jsonb attribute on the model.
f.inputs name: 'JSONB', for: :jsonb_field do |jsb|
jsb.inputs name: 'Foo', for: 'foo' do |x|
x.input :a
x.input :b
end
jsb.inputs name: 'Bar', for: 'bar' do |y|
y.input :c
y.input :d
end
end
I feel like there's a proper way to do this but my research has come up empty. Can anyone help me figure out how to make this happen? Thank you!

Related

Mongoid: inconsistent results on Query when using $elemMatch for nested arrays

Context
puts [
"#{RUBY_ENGINE} #{RUBY_VERSION}",
"Rails gem: #{Rails.gem_version.version}",
"MongoDB #{Mongoid.default_client.command(buildInfo: 1).first[:version]}",
"Mongoid gem: #{Mongoid::VERSION}"
].join("\n")
ruby 2.7.6
Rails gem: 5.2.4.5
MongoDB 4.4.18
Mongoid gem: 7.5.1
Scenario
To this relationships: Foo#bars, Bar#bazes, these are the cases aimed for:
Identify Bar documents where any label of bazes matches cheese.
Identify Bar documents where any id of bazes matches a target id.
Identify Bar documents where bazes have any of an array of ids.
Although I could work out (1), cases (2) and (3) remain a mystery. Below an example, where in Queries and Results there is some example.
Example
Data Model
class Baz
include Mongoid::Document
embedded_in :bar
field :label, type: String
end
class Bar
include Mongoid::Document
embedded_in :foo
embeds_many :bazes, class_name: "Baz"
end
class Foo
include Mongoid::Document
field :name, type: String
embeds_many :bars, class_name: "Bar"
end
Data Sample
foo = Foo.create!({
name: "foo",
bars: [
{bazes: [{label: "table"}, {label: "chair"}]},
{bazes: [{label: "bread"}, {label: "cheese"}]},
{bazes: [{label: "up"}, {label: "down"}]},
{bazes: [{label: "high"}]},
{bazes: [{label: "low"}]}
]
})
Queries and Results
Select the target for cases (1) and (2):
baz = foo.bars[1].bazes.last
baz.label
#=> "cheese"
baz.id
#=> BSON::ObjectId('638d3b824b9f26049b42ac0a')
Case 1
foo.bars.where(bazes: {"$elemMatch": {label: baz.label}}).count
#=> 1
It seems to be working properly. Doing some double check:
bazes = foo.bars.where(bazes: {"$elemMatch": {label: baz.label}}).pluck(:bazes).first
found = bazes.where(label: baz.label).first
#=> #<Baz _id: 638d3b824b9f26049b42ac0a, label: "cheese">
found == baz
#=> true
It certainly worked.
Case 2
Using exactly the same syntax as in Case (1), but matching the id field instead:
foo.bars.where(bazes: {"$elemMatch": {id: baz.id}}).count
#=> 0
It does not work.
At this point I am quite lost. How would a field like label work but the id wouldn't?
Case 3
The below is what I guess it should look like. It doesn't work either, although it makes sense, given that the base case (2) above fails in the first place...
foo.bars.where(bazes: {"$elemMatch": {:id.in => [baz.id]}}).count
#=> 0

Search in rails serialize column

I know it's bad to search in serialize, but I encounter this problem.
I got a model called Question and contain a serialize column assignments
id question_set_id assignments
1 1 {"12982": true, "12332": true}
2 2 {"12222": true, "12332": true}
3 3 {'11111': true}
And I got a array called group_ids
group_ids = ["12982","12332"]
I need to find the record who contain at least one group_id in assignments.
So, the result of this example should be like
[
{
:id => 1,
:question_set_id => 1,
:assignments => {"12982": true, "12332": true}
},
{
:id => 2,
:question_set_id => 2,
:assignments => {"12222": true, "12332": true}
}
]
I've tried
Question.where("assignments IS NOT NULL").where("assignments LIKE '%?%'", 12982)
It seems works, but how to apply an array?
And according this answer, I tried
Question.where("assignments IS NOT NULL").where("assignments= ?", groups_ids.to_yaml)
However, it return a blank array.
With mysql 5.7.? you can query JSON data directly. I didn't try it but following the docs something like
Question.where("JSON_CONTAINS_PATH(assignments , 'one', '$[*].assignments.12982', '$[*].assignments.12332') == 1")
should work. You can furthor more convert your column from text to JSON data type and get validation of the json document and optimized storage.

ActiveRecord where statement selecting fields from nested objects

Given
class Foo
has_many :bar
end
class Bar
belongs_to :foo
end
I want:
=> #<ActiveRecord::Relation [#<Foo id: 11, qux: 'hi', bar_id: 1, bar_name: 'blah', bar_something: 'blahblah' >, #<Foo id: 23, qux: 'hi', bar_id: 2, bar_name: 'lorem', bar_something: 'ipsum' >]>
I can do this:
> Foo.where(qux: 'hi').includes(:bar)
=> #<ActiveRecord::Relation [#<Foo id: 11, qux: 'hi', bar_id: 1 >, #<Foo id: 23, qux: 'hi', bar_id: 2 >]>
But it does not load the child records. It seems just to hold on to it in case it's needed.
There must be something more elegant than this?
Foo.where(qux: 'hi').includes(:bar).to_a.map do | f |
f.keys.each { |k| f[ k.to_s ] = f.delete(k) if k.class == :symbol }
Bar.column_names.except('id','foo_id').each do | ba |
ba_name = 'bar_' + ba
f.merge({ba_name => f.bar.send(ba.to_sym)})
end
f
end
includes(:bar) lazy loads the child records, in this case bar. It's one way to avoid n+1 queries (so that you don't run one query for each instance of foo). And you do have access to it.
Foo.where(qux: 'hi').each do |foo|
puts foo.bar.inspect
end
If you want to get all foos where their bar.qux = hi, then go the otherway:
Bar.joins(:foo).where(foo: { qux: 'hi' })
Foo.select("foos.id,foos.qux,bar_id,bars.bar_name,bars.something").joins(:bar).where(qux: 'hi')
includes lazy load the association so it basically does not merge both tables. What you are looking can be done through joins which allow you to query on both tables and select all required columns which you want. You can find more help here http://tomdallimore.com/blog/includes-vs-joins-in-rails-when-and-where/
Do you actually need the AR relation to have all those values loaded up-front? The lazy loading is intentional to keep you from beating up the DB unnecessarily...
You can still reference any attribute of bar directly:
Foo.where(qux: 'hi').includes(:bar).each do |foo|
puts foo.bar.name # This will actually load bar from the DB
end
There aren't usually great reasons for overriding this, especially if the dataset could be large-ish.

How select one specific field AND arrange by specific key results of mysql query with rails?

I need all name field of my model Topic, but arrange by specific key, in my case the id.
I would like a code like :
Topic.all.pluck_and_arrange_by :id, :name
result:
{ 1 => 'foo', 2 => 'bar', 3 => 'baz' }
where foo is the name of Dialog with id 1 ...etc
What way to do that?
If you are not worried about the sequence of the ids, the below will do
Topic.pluck(:id, :name).to_h #=> { 1 => 'foo', 2 => 'bar', 3 => 'baz' }

ACTIVESCAFFOLD: column sorting?

I have 2 models say 'foo' and 'bar', such as foo has_many :bars, and bar belongs to foo. And they both have a 'name' field.
active_scaffold :bar do |config|
config.columns = [:name, :foo_id]
config.columns[:foo_id].label = 'Foo Name'
end
in the controller and in the helper
def foo_id_column(b)
return link_to b.foo.name, foo_url(b.foo)
end
And this works just fine but whenever I click on the table header "Foo Name" instead of getting sort by the name displayed it is getting sorted by the foo_id, meaning, say we have 2 foo objects "a" with id 2 and "b" with id 1, then on clicking the "FooName" column it is displaying as "b" row first then 'a' instead of 'a' then 'b'. How to change the code so that the activescaffold uses the name content instead of the id for sorting?
As long as the belongs_to/has_many associations are defined in your models, then ActiveScaffold will do all of this for you (including sorting by foo.name) - no custom helper method needed:
active_scaffold :bar do |config|
config.columns = [:name, :foo]
end
This way is also kinder to your DB, since ActiveScaffold will :include the association into the finder for listing bars, avoiding the N-query issue that would be seen in your sample code (fetching each foo record separately when rendering the list of bars)

Resources