How to: Single Table Inheritance in DataMapper? - sinatra

I'm learning Sinatra (1.3.2) and chose to use DataMapper (1.2.0) as ORM and an in-memory SQLite (1.3.6) DB to start.
Two models, Books and Downloads, are sharing most attributes, so I looked into declaring a model for STI (Single Table Inheritance) in DataMapper. Reading the docs, this seems a piece of cake thanks to Types::Discriminator.
I abstracted all common ones into DownloadableResource:
class DownloadableResource
include DataMapper::Resource
property :id, Serial
property :created_at, DateTime
property :modified_at, DateTime
property :active, Boolean, default: true
property :position, Integer
property :title, String, required: true
property :url, URI, required: true
property :description, Text, required: true
property :type, Discriminator
end
Following the example, I thought it's just as easy as specifying what needs to be extended:
class Book < DownloadableResource
property :cover_url, URI
property :authors, String, required: true, length: 255
end
and
class Download < DownloadableResource
property :icon_url, URI
end
but this was giving me the following error:
DataObjects::SyntaxError: duplicate column name: id (code: 1, sql state: , query: ALTER TABLE "downloadable_resources" ADD COLUMN "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, uri: sqlite3::memory:?scheme=sqlite&user=&password=&host=&port=&query=&fragment=&adapter=sqlite3&path=:memory:)
while removing the id generated another (obvious) error:
DataMapper::IncompleteModelError: DownloadableResource must have a key to be valid
I got around this by adding include DataMapper::Resource to both Book and Download, and then Book needed a key to be valid, now looking like this:
class Book < DownloadableResource
include DataMapper::Resource
property :id, Serial
property :cover_url, URI
property :authors, String, required: true, length: 255
end
Same goes for Download, but now the issue is:
DataObjects::SyntaxError: duplicate column name: id (code: 1, sql state: , query: ALTER TABLE "books" ADD COLUMN "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, uri: sqlite3::memory:?scheme=sqlite&user=&password=&host=&port=&query=&fragment=&adapter=sqlite3&path=:memory:)
Starting to feel like I'm going in circles, what's the proper way to implement Single Table Inheritance in DataMapper?
PS: I have looked at
DataMapper - Single Table Inheritance and
Ruby Datamapper table inheritance with associations
but I still have this problem.

I would recommend this approach:
module DownloadableResource
def self.included base
base.class_eval do
include DataMapper::Resource
property :created_at, DateTime
property :modified_at, DateTime
property :active, base::Boolean, default: true
property :position, Integer
property :title, String, required: true
property :url, base::URI, required: true
property :description, base::Text, required: true
property :type, base::Discriminator
end
end
end
class Book
include DownloadableResource
property :id, Serial
# other properties
end
class Download
include DownloadableResource
property :id, Serial
# other properties
end

Related

How do I tell my migration what column type to use?

I'm using Rails 5 and PostGres 9.5. I have created this migration
class CreateSearchCodeTable < ActiveRecord::Migration[5.0]
def change
create_table :search_codes do |t|
t.string :code
t.references :address, index: true, foreign_key: true, on_delete: :cascade
t.index ["code"], name: "index_search_codes_on_code", unique: true, using: :btree
end
end
end
The ID column of the address table is not an integer though. Maybe for that reason, I get the below error when I run the migration
== 20171011202623 CreateSearchCodeTable: migrating ============================
-- create_table(:search_codes)
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:
PG::DatatypeMismatch: ERROR: foreign key constraint "fk_rails_6bd9792e3b" cannot be implemented
DETAIL: Key columns "address_id" and "id" are of incompatible types: integer and character varying.
: CREATE TABLE "search_codes" ("id" serial primary key, "code" character varying, "address_id" integer, CONSTRAINT "fk_rails_6bd9792e3b"
FOREIGN KEY ("address_id")
REFERENCES "addresses" ("id")
)
How do I instruct my migration to create my column with the same type as the referenced column?
t.references delegates job to add_reference and this one accepts :type parameter. Judging from this, you should be able to do
t.references :address, type: :string, index: true, foreign_key: true, on_delete: :cascade
^^^^^^^^^^^^^
Just tested this on a toy sqlite3-based project, it worked. Should "just work" on pg too.
How do I instruct my migration to create my column with the same type as the referenced column?
If you meant to tell it to infer the type of the other column and have type of this one match it, then this is likely not possible. But you can always specify type explicitly.

Modify Devise to support UUID primary key

I want to modify Devise to make it work with a users table with a UUID primary key with PostgreSQL.
Here is the migration:
class DeviseCreateUsers < ActiveRecord::Migration
def change
create_table :users, id: false do |t|
t.uuid :uuid, null: false
# ...
end
change_table :users do |t|
t.index :uuid, unique: true
# ...
end
end
def migrate(direction)
super
if direction == :up
# This is only necessary because the following does not work:
# t.uuid :uuid, primary: true, null: false
execute "ALTER TABLE users ADD PRIMARY KEY (uuid);"
end
end
end
Here is the User model:
class User < ActiveRecord::Base
primary_key = :uuid
devise :database_authenticatable, :recoverable, :registerable,
:rememberable, :trackable, :validatable
validates :uuid, presence: true
before_validation :ensure_uuid
def ensure_uuid; self.uuid ||= SecureRandom.uuid end
end
Here is the error:
PG::Error: ERROR: operator does not exist: uuid = integer
LINE 1: ...ECT "users".* FROM "users" WHERE "users"."uuid" = 1 ORDER...
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT "users".* FROM "users" WHERE "users"."uuid" = 1 ORDER BY "users"."uuid" ASC LIMIT 1
Extracted source (around line #5):
1 .navbar-inner
2 .container
3 = a_nav_tag "App", root_path
4 - if user_signed_in?
5 %ul.nav.pull-right
6 %li.dropdown#user_menu
7 %a.dropdown-toggle(data-toggle="dropdown" href="#")
As you can see above, user_signed_in? is broken. I expect there are several changes needed to move from a 'normal' auto-incrementing ID to a UUID.
For now, I'm just posting the question. I'll take a swing at this later today. If you happen to know how to do this -- or know of a Devise fork, I'd appreciate it.
I've done this in Rails 4 simply by making the id column a uuid data type when creating the table, and no other configuration changes whatsoever. ie. do not create a column named 'uuid', just change the type of the 'id' column to uuid.
Just clear your browser's cookie for the web app (in my case, localhost). The error above is caused because the session was retaining the old user primary key, 1.
After that, things work in my testing. I hope this isn't just luck, it would be a good design if Devise was agnostic about the primary key. (In Devise's code, I saw no use of .id except in some tests.)
2020 answer:
when creating the users table, set the ID as uuid
def change
enable_extension 'pgcrypto' # needed if not already enabled
create_table :users, id: :uuid do |t|
t.string :email,
...

Get all keys in MongoMapper Model including its association

Suppose the following model :
class Product
include MongoMapper::Document
key :name, String
key :product_category_id, ObjectId
belongs_to :product_category
end
class ProductCategory
include MongoMapper::Document
key :name, String, :required => true, :unique => true
timestamps!
userstamps!
end
I want to implement an advanced search that will inspect all value inside my Model including its all association like :
I have :
Product A data named "aLaptop" belongs_to:ProductCategory named "Notebook".
Product B data named "aGreatNotebook" belongs_to:ProductCategory named "Notebook".
When I search with keyword named "Notebook", I want to search it to Product.name fields and also its associations which mean ProductCategory.name also. So it will returned both of that items, because Product A have ProductCategory.name "Notebook" & Product B have Product.name "aGreatNotebook" and ProductCategory "Notebook"..
How can I do this?? I've already searching for 2 days and not success until now:(.. When is in MySQL, I used join table.. But hows in MongoMapper??
Please help.. Thank you..
You can't do joins in MongoDB. So the basic idea is to get the ObjectId associated with the "Notebook" category and then to query the products where product_category is equal to notebook_id. This generally involves two queries. So that'd be something like this:
notebook_id = ProductCategory.first(:name => "Notebook")
if notebook_id
Product.where({:product_category_id => notebook_id['_id']})
end
The question is confusing, but the title of the question is clear.
So in case someone comes here hoping to see how to get all of the:
keys
associations
Read on...
To get the keys in a model:
ConfinedSpace.keys.keys
=> ["_id", "photo_ids", "include_in_qap", "position", "created_at", "updated_at",
"structure_id", "identifier", "name", "description", "notes", "entry_info",
"anchor_points", "nature", "special_equipment", "rescue_overview"]
And to get the associations:
ConfinedSpace.associations.each{|name,assoc| puts name}
photos
attachments
activities
structure
videos
And the class (edited for brevity):
class ConfinedSpace
include MongoMapper::EmbeddedDocument
include Shared::HasPhotos
include Shared::HasAttachments
include HasActivities
TAG = "ConfinedSpace"
belongs_to :structure
many :videos, :as => :attachable
key :identifier, String
key :name, String
key :description, String
key :notes, String
key :entry_info, String
key :anchor_points, String
key :nature, String
key :special_equipment, String
key :rescue_overview, String
validates :identifier, presence: true
end

`execute_non_query': Cannot add a NOT NULL column with default value NULL (DataObjects::SyntaxError)

class User
include DataMapper::Resource
property :id, Serial
property :name, String
property :email, String
has n, :records
end
class Project
include DataMapper::Resource
property :id, Serial
property :name, String
has n, :records ?????
end
#
class Record
# SPEND_REGEX = /^[0-9]{1}:[0-5]{1}[0-9]{1}$/
include DataMapper::Resource
property :id, Serial
property :reporting_type, String
property :spend_time, String
belongs_to :user
belongs_to :project ????
end
DataMapper.auto_upgrade!
With ??? I marked relation that throws an error "`execute_non_query': Cannot add a NOT NULL column with default value NULL (DataObjects::SyntaxError)
"
How to define 2 has many relationships to one model in datamapper?
By default, your belongs_to relationships are required. I assume you already have Record entries in your database. The auto_upgrade is trying to add the new field for the association, and by default it marks that column as NOT NULL. However, for all the existing records, that value will be NULL.
To get around this, do one of the following:
Do an auto_migrate instead of auto_upgrade. This will blow away your data, but
will allow you to add the relationship columns without it choking on
NULL values.
Make the associations optional with :required => false. This will allow NULLs in the database. Next, go in and set those fields to the appropriate values. Lastly, modify the database table column to be NOT NULL.

has_one vs. defining as a Key for Embedded documents for MongoMapper and MongoDB

The Source code is
class RealTimeDetail
include MongoMapper::EmbeddedDocument
key :url, String
key :method, String
end
class TargetFeed
include MongoMapper::Document
key :name, String, :null => false
key :feed_type, String, :null => false
has_one :real_time_detail
end
When I do target_feed.real_time_detail = RealTimeDetail.new(:url => "http://example.com", :method => "get")
I get errored out.
Instead i've changed the TargetFeed to
class TargetFeed
include MongoMapper::Document
key :name, String, :null => false
key :feed_type, String, :null => false
key :real_time_detail, RealTimeDetail
end
This works but was wondering if this is the best way to go about it.
Your classes no longer inherit from ActiveRecord, and has_one belongs to ActiveRecord, so it's not available for use. Using an explicit key seems like it would work.
MongoMapper uses different syntax for implementing associations between data models. Details here: http://mongomapper.com/documentation/plugins/associations.html
the short of it is, instead of has_one, use one
class TargetFeed
include MongoMapper::Document
key :name, String, :null => false
key :feed_type, String, :null => false
one :real_time_detail
end
I believe that since you've defined RealTimeDetail as an embedded document you don't need to declare an association in the definition of RealTimeDetail. If RealTimeDetail included Document (instead of EmbeddedDocument) you would use belongs_to from it's end:
class RealTimeDetail
include MongoMapper::Document
key :url, String
key :method, String
belongs_to :target_feed
end