We have an Item database in Mongoid where we store contextual, optional extra fields for different items, eg:
class Item
include Mongoid::Document
field :name, type: String
end
so I can do something like:
a = Item.new
a.name = "Potato Chips"
a[:flavor] = "Barbecue"
a.save
b = Item.new
b.name = "Underwear"
b[:size] = "XL"
b.save
> Item.first.flavor
=> "Barbecue"
> Item.last.size
=> "XL"
However, say we wanted to do:
class Flavor
include Mongoid::Document
field :name, type: String
field :ingredients, type: Array
end
If you did:
f = Flavor.create({name: "Barbecue", ingredients: ["salt", "sugar"]})
a[:flavor] = f
a.save
You get:
NoMethodError: undefined method `__bson_dump__' for #<Flavor:0x007fb34d3c1718>
How do I make it so I can go:
Item.first.flavor.ingredients[0]
Related
I'm to insert a record with embedded field in OrientDB and then to query for that record using filter:
insert into MyClass set embeddedField = {'#type': 'd', 'id': 1}
works well, and
select from MyClass
returns the record I've added. But when I add where with filter for embeddedField, I get no results:
select from MyClass where embdeddedField = {'#type': 'd', 'id': 1}
I thought that it could happen because Orient adds #version field into embedded document, so I tried to search with version:
select from MyClass where embdeddedField = {'#type': 'd', '#version': 0, 'id': 1}
But still got no results.
Question: Any idea how to filter embedded field by entire document? Without the need to filter explicitly by each field of embedded document:
select from MyClass
where embdeddedField.id = 1
and embdeddedField.field2 = val2
and embeddedField.field3 = val3
Because for several reasons I would like to pass the entire object as single query parameter:
select from MyClass where embdeddedField = ?
Thats because the provided Hash is transformed to an embedded class.
Perhaps take a look at the ActiveOrientWiki https://github.com/topofocus/active-orient/wiki/Relations
To explain:
Take a class base, there a document with two properties (schemaless)
and add existing documents to it
( 0..9 ).each do | b |
base_record= Base.create label: b, first_list: []
( 0..9 ).each {|c| base_record.first_list << FirstList.create( label: c ) }
end
INFO->CREATE VERTEX base CONTENT {"label":0,"first_list":[]}
INFO->CREATE VERTEX first_list CONTENT {"label":0}
INFO->update #136:0 set first_list = first_list || [#147:0] return after #this
then links are embedded and the record looks like
=> #<Base:0x00000000041a60e0 #metadata={:type=>"d", :class=>"base", :version=>11, :fieldTypes=>"first_list=z", :cluster=>129, :record=>1},
#attributes={:first_list=>["#149:1", "#150:1", "#151:1", "#152:1", "#145:2", "#146:2", "#147:2", "#148:2", "#149:2", "#150:2"], :label=>"1"}>
if you expand the list items, you can query for "#type"
If you add a document without rid, the same thing happens
( 0..9 ).each {|c| base_record.first_list << FirstList.new( label: c ) }
20.04.(12:53:59) INFO->update #130:2 set first_list = first_list || [{ #type: 'd' ,#class: 'first_list' ,label: 0 }] return after #this
INFO->CREATE VERTEX base CONTENT {"label":"c","first_list":[]}
'#130:2'.expand
=> #<Base:0x00000000043927a0 #metadata={:type=>"d", :class=>"base", :version=>11, :fieldTypes=>"first_list=z", :cluster=>130, :record=>2},
#attributes={:first_list=>["#151:12", "#152:12", "#145:13", "#146:13", "#147:13", "#148:13", "#149:13", "#150:13", "#151:13", "#152:13"], :label=>"c"}>
If you omit the class-name, only the scope of the rid's changes
INFO-> update #132:2 set first_list = first_list || { #type: 'd' ,label: 0 } return after #this
=> [#<Base:0x0000000003a26fb8 #metadata={:type=>"d", :class=>"base", :version=>3, :fieldTypes=>"first_list=z", :cluster=>132, :record=>2},
#attributes={:first_list=>["#3:0"], :label=>"d"}>]
In any case, a query for #type failes
class Building < ActiveRecord::Base
# id
# name
# url
has_many :floors
end
class Floor < ActiveRecord::Base
# id
# building_id
# name
# number
belongs_to :building
end
I want to add a method to the Building model that returns the following:
def self.some_stuff
[{
id: "81b0bd96-20e9-4f01-9801-59f3d9b99735",
url: "http://www.google.com",
numbers: [4,5,6,7,8,9]
},
{
id: "5de096dd-f282-41cc-8300-b972fb61ea41",
url: "http://www.yahoo.com",
numbers: [2,7,11]
}]
end
Ideally I would like to make this chainable so that I could do the following:
Building.all.limit(100).some_stuff
EDIT 1:
Here is how what I am trying to get at looks like in postgresql:
SELECT
B.id,
B.url,
array_agg(F.floor_number) AS numbers
FROM
buildings AS B
LEFT OUTER JOIN floors AS F ON
F.building_id = B.id
GROUP BY
B.uuid;
EDIT 2:
The following gives me the data I want but in a really crappy format (because of pluck). If I change pluck to select it breaks.
def self.some_stuff
fields = "buildings.id, buildings.url"
joins(:floors)
.includes(:floors)
.group(fields)
.pluck("#{fields}, array_agg(floors.floor_number)")
end
You likely want your some_stuff method to look something like this:
class Building
def self.some_stuff
map do |building|
{
id: building.id,
url: building.url,
numbers: building.floors.pluck(:number)
}
end
end
end
def self.some_stuff
includes(:floors).map do |b|
{
id: b.id,
url: b.url,
numbers: b.floors.map(&:number)
}
end
end
Slight modification of Josh's answer. Added the includes and changed pluck to map to avoid n+1 queries.
I have two models, Ownerand Property, where the schema for Ownerhas an embeds_many declaration, like this:
defmodule MyApp.Owner do
use MyApp.Web, :model
alias MyApp.Property
schema "owners" do
field name, :string
embeds_many :properties, Property
timestamps()
end
#doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [])
|> validate_required([])
end
end
and this:
defmodule MyApp.Property do
use MyApp.Web, :model
embedded_schema do
field :name, :string
field :value, :float, default: 0
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name, :value])
|> validate_required([:name])
end
end
The migration I'm using is:
defmodule MyApp.Repo.Migrations.CreateOwner do
use Ecto.Migration
def down do
drop table(:owners)
end
def change do
drop_if_exists table(:owners)
create table(:owners) do
add :name, :string
add :properties, :map
timestamps()
end
end
end
And a possible seed is:
alias MyApp.{Repo, Owner, Property}
Repo.insert!(%Owner{
name: "John Doe",
properties: [
%Property{
name: "Property A"
},
%Property{
name: "Property B",
value: 100000
},
%Property{
name: "Property C",
value: 200000
}
]
})
Finally, my questions are: how can I update John Doe's Property C's value from 200000to 300000? And if John Doe buys a Property D:
%Property{
name: "Property D"
value: 400000
}
How do I add that to his properties in the database? (I'm using Postgres).
The simplest way would be to fetch the record, update the properties
list and save the changes:
owner = Repo.get!(Owner, 1)
properties = owner.properties
# update all properties with name "Property C"'s value to 400000
properties = for %Property{name: name} = property <- properties do
if name == "Property C" do
%{property | value: 400000}
else
property
end
end
# add a property to the start
properties = [%Property{name: "Property D", value: 400000} | properties]
# or to the end
# properties = properties ++ [%Property{name: "Property D", value: 400000}]
# update
Owner.changeset(owner, %{properties: properties})
|> Repo.update!
You can do some operations (at least inserting a property) using the JSON functions
provided by PostgreSQL using
fragment but I don't think you can search and conditionally update an item
of an array using them.
I am using mongoid(2.6.0) with its alias and this is how my model field looks like
class Place
include Mongoid::Document
field :n, :as => :name, :type => String
....
Now I have a controller which finds a place and return the object as json
#places = Place.find({some query})
respond_to do |format|
format.json { render json: #places }
end
Now when I do
JSON.parse(response.body)
My response contains the field as "n" and not as "name".
Is there a way I can ask mongoid to return me the alias name and not the actual name?
Well you can try overriding serializable_hash method. Just add something like this in your model.
def serializable_hash(options)
original_hash = super(options)
Hash[original_hash.map {|k, v| [self.aliased_fields.invert[k] || k , v] }]
end
I have the following model:
class User
include Mongoid::Document
store_in :users
field :full_name, :type => String
end
class Message
include Mongoid::Document
embeds_one :sender, :class_name => "User"
field :text, :type => String
end
I would like to store User and Message in separated standalone collections so that they could be queried directly, and I would like to have one copy of user for sender in each Message entry. Is my model correct for this kind of request?
And when I have an instance of User user, how could I query the messages where sender = user?
I've tried:
Message.where(:sender => user)
Message.where('sender.id' => user.id)
both not work.
only Message.where('sender.full_name' => user.full_name) worked, but I don't want to rely on a text field when there's an id field to use.
What's the best way to do that?
How I save Message/User:
user = User.new
user.full_name = 'larry'
user.save
m = Message.new(:text => 'a text message')
m.sender = user
m.save
And it results in the database:
> db.users.find({ 'full_name' : 'larry'})
> db.messages.find({})[0]
{
"_id" : ObjectId("4f66e5c10364392f7ccd4d74"),
"text" : "a text message",
"sender" : {
"_id" : ObjectId("4f62e0af03642b3fb54f82b0"),
"full_name" : "larry"
}
}
Like explain by Jordan Durran ( Mongoid lead developer ) in Google group of Mongoid : http://groups.google.com/group/mongoid/browse_thread/thread/04e06a8582dbeced#
You're going to need a separate model if you want to embed the user
data inside the message. When denormalizing like this I generally
namespace one of them, and create a module with the common fields to
include in both - maybe in your case you can call it Sender?
class Sender
include Mongoid::Document
include UserProperties
class << self
def from_user(user)
Sender.new(user.attributes)
end
end
end
class User
include Mongoid::Document
include UserProperties
end
module UserProperties
extend ActiveSupport::Concern
included do
field :full_name, type: String
end
end
class Message
include Mongoid::Document
embeds_one :sender
end
You also don't need the :store_in macro on User - by default it's name
would be "users".
You can't do what you do.
Your user document is save in his one collection because you use the store_in method. And you try save it on an other document ( Message)
If you really want 2 collections, you need use has_one :user in your Message class.
class Message
has_one :sender, :class_name => 'User', :foreign_key => 'sender_id'
end
After you can get your message like :
Message.senders to have all of your sender.