I have a use case to store arbitrary objects (classA or classB or classC) in a column of a database table. Then when I read it, I would like to deserialize it back to the original class (A or B or C) Is it possible to do that?
It seems like I can serialize the object to json, and deserialize it to OpenStruct object. But I am hoping to get the original object so I can send it as a response which can be recognized by my protobuf definition.
[Edit]:
One extra requirement is that my server will have dependency to class A, B, C... but I have no permission to change it. Ideally I want to avoid adding extra initialize or serialize method to it.
The data stored in the database ideally could be readable after someone execute sql query. (not stored in binary format)
What I have tried is to:
Convert the data to hash then to json:
a = classa_obj.to_h.to_json
Store it to db:
active_record_obj.col_a = a
active_record_obj.save
Retrieve the record from db:
query_result = # sql query filter by the id
Covert it the json part back to class A object:
classa_obj = ClassA.new(Json.parse(query_result.col_a))
This seems to work but I am not sure if it apply to some more complicated class structure
Lets say you have a active record class called User and you wanted to store a class A object in a column of a User. Then you need to define that column as json type and store the data in database as hash. When you want to retrieve it you can initialize a class A object to deserialize it. Here is the code example
class A
attr_reader :attr1, :attr2, :attr3
def initialize(attr1:, attr2:, attr3:)
#attr1 = attr1
#attr2 = attr2
#attr3 = attr3
end
def attributes
{
attr1: attr1,
attr2: attr2,
attr3: attr3
}
end
end
class User < ApplicationRecord
# class_object => json type column in users table
def class_object
A.new(**self[:class_object])
end
end
a = A.new(attr1: 1, attr2: 2, attr3: 3)
user = User.create(class_object: a.attributes)
user.class_object # => will return object from class A
Related
I'm trying to transfer all the information from my Ruby file into a Postgres database. I am able to transfer the information when I do not have an array column, so I am assuming the error message I am getting is because of the array column I am trying to add. The error message I am getting is:
in `exec_prepared': ERROR: missing dimension value (PG::InvalidTextRepresentation)
Here is the code I used to connect my Ruby file to my Postgres database:
require 'pg'
class Postgres
# Create the connection instance. Scraping is the name of the database I am adding this information to
def connect
#conn = PG.connect(:dbname => 'scraping')
end
# Create our venue table
def createVenueTable
#conn.exec("CREATE TABLE venues (venue_number varchar(15) UNIQUE,...,img_array varchar[]);")
end
...
def prepareInsertVenueStatement
#conn.prepare("insert_venue", "insert into venues(venue_number,...,img_array) values ($1,...,$24)")
end
# Add a venue with the prepared statement.
def addVenue(venue_number,...,img_array)
#conn.exec_prepared("insert_venue", [venue_number,...,img_array])
end
end
When I check my Postgres database, the img_array column is made, however, I am unable to populate it. Please help! Thank you.
I would suggest using serialization to handle this so that you are actually just writing a string rather than an actual array.
require 'pg'
require 'yaml'
class Postgres
# Create the connection instance. Scraping is the name of the database I am adding this information to
def connect
#conn = PG.connect(:dbname => 'scraping')
end
# Create our venue table
def createVenueTable
#changed img_array to a varchar(8000) for storing serialized Array
#conn.exec("CREATE TABLE venues (venue_number varchar(15) UNIQUE,...,img_array varchar(8000));")
end
...
def prepareInsertVenueStatement
#conn.prepare("insert_venue", "insert into venues(venue_number,...,img_array) values ($1,...,$24)")
end
# Add a venue with the prepared statement.
def addVenue(venue_number,...,img_array)
#conn.exec_prepared("insert_venue", [venue_number,...,serialized(img_array)])
end
#serialize the Object
def serialized(obj)
YAML.dump(obj)
end
#deserialize the Object
def deserialized(obj)
YAML.load(obj)
end
end
Abstracted Usage Example just to show serialization
a = [1,2,4,5]
serialized = YAML.dump(a)
#=> "---\n- 1\n- 2\n- 3\n- 4\n- 5\n"
YAML.load(serialized)
#=> [1,2,3,4,5]
#Also works on Hash Objects
h = {name: "Image", type: "jpeg", data:[1,2,3,4,5]}
serial = YAML.dump(h)
#=> "---\n:name: Image\n:type: jpeg\n:data:\n- 1\n- 2\n- 3\n- 4\n- 5\n"
YAML.load(serial)
#=> {:name=>"Image", :type=>"jpeg", :data=>[1, 2, 3, 4, 5]}
Hope this helps you out with handling this issue.
If you need to store more than 8000 characters you can switch to varchar(MAX) or text column definitions. I would recommend varchar(MAX) because the data will be stored as a standard varchar until it exceeds 8000 character at which point the db basically converts it to a text column under the hood.
Background
The Entity class is a base class that gets inherited by several subclasses that holds entities received over a REST API. The entity classes are immutable and should return a new instance of themselves whenever a change is attempted.
The Entity class has an .update() method that takes a hash of values to update, if the changes aren't really changes it returns itself and if there are real changes it returns a new instance of itself with the changes effected before instantiation.
To be user friendly Entity also allows for direct assignment to properties (so that if a subclass of Entity has a name attribute you can do instance.name = 'New Name') that also returns a new instance of the class. This is implemented in terms of update using dynamic methods that are created when the class is instantiated.
And they are the problem.
Problem
The code in the Entity class looks, in part, like this (for a complete code listing and tests check out the Github repo: https://github.com/my-codeworks/fortnox-api.git):
require "virtus"
require "ice_nine"
class Entity
extend Forwardable
include Virtus.model
def initialize( hash = {} )
super
create_attribute_setter_methods
IceNine.deep_freeze( self )
end
def update( hash )
attributes = self.to_hash.merge( hash )
return self if attributes == self.to_hash
self.class.new( attributes )
end
private
def create_attribute_setter_methods
attribute_set.each do |attribute|
name = attribute.options[ :name ]
create_attribute_setter_method( name )
end
end
def create_attribute_setter_method( name )
self.define_singleton_method "#{name}=" do | value |
self.update( name => value )
end
end
end
Doing this:
instance.update( name: 'New Name' )
and this:
instance.name = 'New Name'
Should be the same, literally since one is implemented in terms of the other.
While .update() works perfectly the .attr=() methods return the value you assign.
So in the above example .update() returns a new instance of the Entity subclass but .attr=() returns 'New Name' ...
I have tries capturing the output inside the .attr=() method and log it before returning so that I have this:
self.define_singleton_method "#{name}=" do | value |
p "Called as :#{name}=, redirecting to update( #{name}: #{value} )"
r = self.update( name => value )
p "Got #{r} back from update"
return r
end
And the log lines say:
"Called as :name=, redirecting to update( name: 'New Name' )"
"Got #<TestEntity:0x007ffedbd0ad18> back from update"
But all I get is the string 'New Name'...
My forehead is bloody and no posts I find show anything close to this. I bet I'm doing something wrong but I can't find it.
Getting dirty
The Github repo has tests in rspec that you can run, the failing ones are focused right now and some extra logging is in the Entity class to capture the different internal steps.
Comments, links and/or pull requests are welcome.
Turns out that the = methods always return the value being assigned.
o = Struct.new(:key).new(1)
o.define_singleton_method("something") { #something }
o.define_singleton_method("something=") do |v|
#something = v
return 6
end
As you can see, I've 'fixed' the return value to 6 each time something= is called. Let's see if it works:
o.something = 1 #=> outputs 1, not 6
o.something #=> outputs 1, so the method did indeed run
Conclusion? My guess is that an = method will return the value that you are assigning through it. And IMO it's better this way; one reason would be to ensure proper functioning of assignment chains:
new_val = o.something = some_val
In cucumber, one of the best feature is the Table data passing. However if I want to add additional data to it, or create a Table data in my step_definitions, how could I do that? What type is Table (hash? map? list? array?)?
To illustrate, below is one of my step, accepting a table data from the feature, and pass along to a function. I like to append some data to it. How could I do that?
Then(/^posted JSON should have the below attributes$/) do |table|
## Here I want to append some data to my table. How to do it?
posted_json_attribute_table_check table
end
Then I have a function that use it to compare with a read JSON.
def posted_json_attribute_table_check(table)
json = JSON.parse $post_result.lines.first
data = table.raw
data.each do |entry|
status = entry[0]
value = entry[1]
expect(json[status].to_s).to eq(value)
end
end
Thanks!
The table object is of type Cucumber::Core::Ast::DataTable and can be found here. https://github.com/cucumber/cucumber-ruby-core/blob/master/lib/cucumber/core/ast/data_table.rb
# Creates a new instance. +raw+ should be an Array of Array of String
# or an Array of Hash
# You don't typically create your own DataTable objects - Cucumber will do
# it internally and pass them to your Step Definitions.
#
def initialize(raw, location)
raw = ensure_array_of_array(rubify(raw))
verify_rows_are_same_length(raw)
#raw = raw.freeze
#location = location
end
When creating a record the URL generated to view that record ends with its id
/record/21
I would like to be able to change that to something easier to read, such as my name and reference attributes from the model. I have looked at friendly_id but has trouble implementing a custom method to generate the URL
class Animal < ActiveRecord::Base
extend FriendlyId
friendly_id :name_and_ref
def name_and_ref
"#{name}-#{reference}"
end
end
I ended up getting an error
PG::UndefinedColumn: ERROR: column animals.name_and_ref does not exist LINE 1: SELECT "animals".* FROM "animals" WHERE "animals"."name_an... ^ : SELECT "animals".* FROM "animals" WHERE "animals"."name_and_ref" = 'Clawd-A123456' ORDER BY "animals"."id" ASC LIMIT 1
def show
#animal = Animal.friendly.find(params[:id])
end
I then come across the to_param method which Rails has available, in my model I have
def to_param
"#{self.id}-#{self.name}"
end
which will generate a URL for me of
/19-clawd
This works, but when I do the following it throws an error
def to_param
"#{self.name}-#{self.reference}"
end
My question though is how can I generate the URL to be name and reference without it throwing
Couldn't find Animal with 'id'=Clawd-A123456
If you would like to use your own "friendly id" then you'll need to adjust the find statement in your controller to something like
id = params[:id].split(/-/, 2).first
#animal = Animal.find(id)
Similarly, for the name/reference combination
name, reference = params[:id].split(/-/, 2)
#animal = Animal.find_by(name: name, reference: reference)
The second choice is a little more difficult because you'll have to do some work in the model to guarantee that the name/reference pair is unique.
The easiest way, is to go with friendly_id and simply add the missing database column. Keep in mind that you will need to ensure this new column is unique for every record. It basically acts as primary key.
Question: How can i use for one field both sequence and transient attribute?
Background: I have factory, which has a name. The name is sequence to keep it unique. However in few specs i need it set name chosen by me so i can predict it in expectation. It's not a Rails project.
In my head it looks like name {attribute_from_create_call||FactoryGirl.generate :name}. But i don't know how to get the attribute which i give to the create method
Factory:
FactoryGirl.define do
sequence :name do |n|
'Testing Bridge '+n.to_s
end
factory :historical_bridge do
name {FactoryGirl.generate :name}
end
end
Usage of factory: FactoryGirl.create :historical_bridge, name: 'Bridge from '+Time.now.to_s
You can use FactoryGirl to create a hash of attributes with the sequences and then merge in whatever changes to that hash you want:
new_name = 'Bridge from '+Time.now.to_s
attr = FactoryGirl.attributes_for(:historical_bridge).merge(:name => new_name)
And then you could do something perhaps like create an object with those custom attributes:
HistoricalBridge.create(attr)
I think you should not use predefined name instead of sequence but check something like:
let(:bridge) { create :historical_bridge }
it { HistoricalBridge.something.name.should == bridge.name }