[RoR]Korte URL's bij geneste data

Pagina: 1
Acties:

Onderwerpen


Acties:
  • 0 Henk 'm!

  • djluc
  • Registratie: Oktober 2002
  • Laatst online: 19-09 16:12
Ik ben druk bezig met Ruby on Rails, daarbij maak ik een geneste applicatie vergelijkbaar met een bugtracker.
Models:
Project
Part
Task
Note

Alles hangt onder elkaar dus. Eerst had ik nested routes gebruikt, maar dat werkt zeer vervelend omdat je dan project_part_task_path e.d. gaat krijgen. Erg lange paden dus, de URL in de browser maakt me overigens niets uit. Het gaat om programmeer gemak.

Nu heb ik dit:
Ruby:
1
2
3
4
  map.resources :projects
  map.resources :parts, :path_prefix => '/projects/:project_id'
  map.resources :tasks, :path_prefix => '/projects/:project_id/parts/:part_id'
  map.resources :notes, :path_prefix => '/projects/:project_id/parts/:part_id/tasks/:task_id'


Werkt opzich wel leuk, het zorgt nog steeds voor een nested url, dus http://localhost:3000/projects/1/parts/1/tasks/9 maar je hoeft niet meer van die lange functienamen te gebruiken.

Vraag: Is dit de enige en goede manier? Ik kwam namelijk ook dit (door de officiele site geadviseerde) artikel tegen: http://weblog.jamisbuck.org/2007/2/5/nesting-resources

Ik zie echter niet in hoe zij als ze bijvoorbeeld part_path(4) doen weten welk project het is.

Acties:
  • 0 Henk 'm!

  • mithras
  • Registratie: Maart 2003
  • Niet online
Ik ken verder RoR niet, maar ik neem aan dat als je naar specifiek task id #284 verwijst, dat je dan weet dat dit bij part #18 hoort in project #9182. Dus heb je niet aan /projects/9182, /task/283 of /part/18 genoeg :?

Verder, uit de php wereld met ZF gebruiken ze ook die vorm van urls: http://domain.tld/var1/value1/var2/value2/var3/value3. Imho de meest nette manier om het te structureren (en vervolgens weer uit elkaar te trekken).

[ Voor 33% gewijzigd door mithras op 13-01-2010 16:57 ]


Acties:
  • 0 Henk 'm!

  • Leftblank
  • Registratie: Juni 2004
  • Laatst online: 20:50
djluc schreef op woensdag 13 januari 2010 @ 16:44:
Vraag: Is dit de enige en goede manier? Ik kwam namelijk ook dit (door de officiele site geadviseerde) artikel tegen: http://weblog.jamisbuck.org/2007/2/5/nesting-resources

Ik zie echter niet in hoe zij als ze bijvoorbeeld part_path(4) doen weten welk project het is.
Ik zou zelf in ieder geval de tip volgen uit het artikel; nest je routes niet te diep. Geneste routes zijn handig wanneer je het nodig hebt om te achterhalen over welk object je 't hebt (project/1/parts bijv). Wanneer je 't echter over concrete dingen hebt, zoals je part_path(4) dan kun je beter gewoon gebruik maken van je models om op die manier de parent te achterhalen.

Op die manier blijven je schoner maar het zelfde geldt voor je named routes, je zou nu voor je notes routes veel argumenten mee moeten geven die je zelf ook kunt achterhalen dmv. je models, mits je de juiste relaties legt. Ik weet niet in hoeverre je al op de hoogte bent van de vele Railssites en opties, maar wellicht kun je ook nog wat tips opdoen met Railscasts over nested resources (en andere topics).

Acties:
  • 0 Henk 'm!

  • djluc
  • Registratie: Oktober 2002
  • Laatst online: 19-09 16:12
Bedankt voor de tips tot op heden! Jullie idee om dit via de models te spelen is wel interessant. Wat ik nu doe in de controller is het volgende:

In de note controller, de diepste bijvoorbeeld:
Ruby:
1
2
3
4
5
6
7
8
9
10
11
12
class NotesController < ApplicationController
  before_filter :login_required, :defProject, :defPart, :defTask
  
  def defProject
    @project = Project.find(params[:project_id])
  end
  def defPart
    @part = Part.find(params[:part_id])
  end
  def defTask
    @task = Task.find(params[:task_id])
  end


Ik moet nu dus wel bij elke link in mijn view iets als dit doen:
Ruby:
1
2
<%= link_to 'Edit', edit_note_path(@project, @part, @task, @note) %> 
|

Acties:
  • 0 Henk 'm!

  • Leftblank
  • Registratie: Juni 2004
  • Laatst online: 20:50
Mmm, je hebt 't idee nog niet helemaal opgepakt zie ik. Ik neem aan dat je models er uitzien zoals: (Zo niet dan is dat al een puntje om naar te kijken ;))
Ruby:
1
2
3
4
5
6
7
8
9
10
class Note < ActiveRecord::Base
  belongs_to :task
end

class Task < ActiveRecord::Base
   belongs_to :Part
   has_many :tasks
end

# etc


Wanneer dat wel het geval is kun je in je controller gewoon gebruik maken van de functies die ActiveRecord je biedt:
Ruby:
1
2
3
4
5
6
7
8
9
def show
  note = Note.find(params[:id])
  # Task bijv:
  task = note.task
  # Eventueel te optimaliseren door gebruik te maken van het vooraf meeladen ipv losse queries wanneer nodig
  note_met_task_preloaded = Note.find(params[:id], :include => :task)
  # En te beveiligen door via de ingelogde user te zoeken:
  safe_task = current_user.notes.find(params[:id])
end

[ Voor 3% gewijzigd door Leftblank op 14-01-2010 22:45 ]


Acties:
  • 0 Henk 'm!

  • djluc
  • Registratie: Oktober 2002
  • Laatst online: 19-09 16:12
models:
Ruby:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Note < ActiveRecord::Base
  belongs_to :task
end
class Task < ActiveRecord::Base
  belongs_to :part
  has_many :notes
end
class Part < ActiveRecord::Base
  belongs_to :project
  has_many :tasks
end
class Project < ActiveRecord::Base
  has_many :parts
  validates_presence_of :name, :description
end


controller voorbeeld notes_controller
Ruby:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class NotesController < ApplicationController
  before_filter :login_required, :defProject, :defPart, :defTask
  
  def defProject
    @project = Project.find(params[:project_id])
  end
  def defPart
    @part = @project.parts.find(params[:part_id])
  end
  def defTask
    @task = @part.tasks.find(params[:task_id])
  end
  
  def index
    @notes = @task.notes

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @notes }
    end
  end

  # GET /notes/1
  # GET /notes/1.xml
  def show
    @note = @task.notes.find(params[:id])
  end
end


voorbeeld view show note
Ruby:
1
2
3
4
5
6
7
8
9
<p>
  <b>Message:</b>
  <%=h @note.message %>
</p>



<%= link_to 'Edit', edit_note_path(@project, @part, @task, @note) %> |
<%= link_to 'Back', notes_path(@project, @part, @task) %>


Het grappige hiervan is dat je sowieso geen fout pad kan hebben, dus een note die niet bij een project hoort. Die def functies wil ik dan bijvoorbeeld in de application controller plaatsen zodat je gewoon kan gebruiken wat je wilt.

Maar ik zou dus in die view al die lange paden weg willen hebben, dus gewoon edit_note_path(@note). Dus dat ik iets fix waardoor die applicatie zelf gaat verzinnen waar de note dan bij hoort.

[ Voor 14% gewijzigd door djluc op 14-01-2010 23:38 ]


Acties:
  • 0 Henk 'm!

  • Leftblank
  • Registratie: Juni 2004
  • Laatst online: 20:50
djluc schreef op donderdag 14 januari 2010 @ 23:34:
Het grappige hiervan is dat je sowieso geen fout pad kan hebben, dus een note die niet bij een project hoort. Die def functies wil ik dan bijvoorbeeld in de application controller plaatsen zodat je gewoon kan gebruiken wat je wilt.
Dat check je dus niet; je models controleren niet of er wel een 'parent' gekozen is, op deze manier kun je dus wel fouten krijgen in je code wanneer de user iets verkeerds invult.
Maar ik zou dus in die view al die lange paden weg willen hebben, dus gewoon edit_note_path(@note). Dus dat ik iets fix waardoor die applicatie zelf gaat verzinnen waar de note dan bij hoort.
Dat 'zelf gaan verzinnen' kun je dus via de relaties regelen zoals ik even hierboven heb laten zien. Het enige wat je zal moeten doen is je models aanpassen zodat ze ook controleren of er een parent is gekozen, op die manier zul je ook geen 'orphans' kunnen krijgen (mits je ook de children delete bij verwijderen van de parent, dat kan bijv met. :depends => :delete_all op je model relaties)

Acties:
  • 0 Henk 'm!

  • djluc
  • Registratie: Oktober 2002
  • Laatst online: 19-09 16:12
Sorry, super dat je me wilt helpen maar ik snap het niet. Wat klopt er niet aan de models? Die zijn toch allemaal met echte relaties verbonden? Waarschijnlijk zie ik het niet!

Wat betreft de controle: Door de echter relaties zal de eerste url wel werken en de 2de niet:
http://localhost:3000/projects/1/parts/1/tasks/9
http://localhost:3000/projects/2/parts/1/tasks/9

Dit omdat ik in de def functies kijk naar de parent?

Je toont me een voorbeeld uit de controller, daar heb ik voor zover ik weet op dit moment geen probleem. Het probleem zit 'm waarschijnlijk in de views? Die hebben verkeerde info waardoor ze hun path niet kennen?

Acties:
  • 0 Henk 'm!

  • Leftblank
  • Registratie: Juni 2004
  • Laatst online: 20:50
djluc schreef op vrijdag 15 januari 2010 @ 03:23:
Sorry, super dat je me wilt helpen maar ik snap het niet. Wat klopt er niet aan de models? Die zijn toch allemaal met echte relaties verbonden? Waarschijnlijk zie ik het niet!
Je mist in de models nog een validates_presence_of de 'parent' van elk model, zoals je 't nu hebt opgesteld kun je prima een Note aanmaken zonder een Task, waarna je app de mist in zal gaan wat betreft de urls.

Mijn suggestie is dan ook om je geneste urls te laten vallen (of met Rails zelf op te bouwen) en die validatie in je models te doen zodat je de validatie door je model laat doen, en niet je urlstructuur ;)

Acties:
  • 0 Henk 'm!

  • djluc
  • Registratie: Oktober 2002
  • Laatst online: 19-09 16:12
Ok, uiteraard heb ik die toegevoegd, inderdaad een sterk idee om dat te doen zodat het nooit fout kan gaan in theorie.

[quote]Mijn suggestie is dan ook om je geneste urls te laten vallen (of met Rails zelf op te bouwen) en die validatie in je models te doen zodat je de validatie door je model laat doen, en niet je urlstructuur [/quote]
Ok, als ik nested urls laat vallen, hoe weet een note dan bij welk project hij hoort? Ga je dan de andere kant op zoeken? Dus note -> task -> part -> project.id?


edit Ok, ik ben er weer een heel eind verder mee. Ik heb de routes kort gemaakt, exact volgens het artikel. Ik ben nu weer aangekomen bij formulieren e.d. de linkjes werken gewoon.

Ik wil nu onder een project een nieuw part aanmaken.
http://localhost:3000/projects/1/parts/new
Klopt het dat ik dan in de controller de project_id uit de url moet toevoegen zoals onderstaand om de relatie te leggen?
Ruby:
1
2
3
4
5
6
7
8
9
10
11
12
class PartsController < ApplicationController

  def create
    @project = Project.find(params[:project_id])
    @part = Part.create(params[:part])
    @part.project_id = @project.id
    if @part.save
      redirect_to part_url(@part)
    else
      render :action => "new"
    end
  end

[ Voor 42% gewijzigd door djluc op 15-01-2010 21:02 ]


Acties:
  • 0 Henk 'm!

  • Leftblank
  • Registratie: Juni 2004
  • Laatst online: 20:50
djluc schreef op vrijdag 15 januari 2010 @ 19:19:
Ik wil nu onder een project een nieuw part aanmaken.
http://localhost:3000/projects/1/parts/new
Klopt het dat ik dan in de controller de project_id uit de url moet toevoegen zoals onderstaand om de relatie te leggen?
Dat is de ene manier, als alternatief zou je met 'n hidden form field kunnen werken, maar dat is natuurlijk net wat minder veilig. Overigens kun je - als het goed is - ook gewoon params[:project_id] pakken, als je de routes goed zijn ingesteld. Mocht je je @project toch nodig hebben kun je dit overslaan natuurlijk ;).

Acties:
  • 0 Henk 'm!

  • messi
  • Registratie: Oktober 2001
  • Laatst online: 18:52
Doe het dan helemaal netjes en gebruik de project relatie met parts om de part te createn.

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PartsController < ApplicationController
  
  before_filter :load_project
  
  def create
    @part = @project.parts.create(params[:part])
    if @part.valid?
      redirect_to part_url(@part)
    else
      render :action => "new"
    end
  end
  
  def load_project
    @project = Project.find(params[:project_id])
  end
end



Overigens is parts.create() en parts.save() dubbelop, want create() saved al direct naar de database.
Je kan dan beter valid? gebruiken, scheelt je weer een database call.
(of parts.new() en dan @part.save())

Met de beforefilter haal je een hoop duplicate code weg als je toch gaat scopen op project.
nu kun je in de show method bijvoorbeeld doen:

code:
1
2
3
def show
  @project.parts.find(params[:id])
end


je kan het nog minder duplicate maken door het volgende:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class PartsController < ApplicationController
  
  before_filter :load_project
  before_filter :load_part, :not => [:index]
  
  def index
    @parts = @project.parts
  end

  def create
    @part = @project.parts.create(params[:part])
    if @part.valid?
      redirect_to part_url(@part)
    else
      render :action => "new"
    end
  end
  
  def show
  end
  
  def update
    @part.update_attributes(params[:part])
  end
  
  private
  
  
  def load_part
    @part = @project.parts.find(params[:id])
  end
  
  def load_project
    @project = Project.find(params[:project_id])
  end
end


typo's voorbehouden :p

[ Voor 56% gewijzigd door messi op 16-01-2010 12:40 ]

Onze excuses voor het ontbreken van de ondertiteling.


Acties:
  • 0 Henk 'm!

  • djluc
  • Registratie: Oktober 2002
  • Laatst online: 19-09 16:12
Dat is nog net wat netter inderdaad, had dit:
Ruby:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class PartsController < ApplicationController
  before_filter :login_required

  def create
    @project = Project.find(params[:project_id])
    @part = Part.create(params[:part])
    @part.project_id = @project.id
    if @part.save
      redirect_to part_url(@part)
    else
      render :action => "new"
    end
  end
end

Welke de volgende form tag genereert:
<form action="/parts" class="new_part" id="new_part" method="post"

Daar heb ik nu dit van gemaakt:
Ruby:
1
2
3
4
5
6
7
8
9
def create
    @project = Project.find(params[:project_id])
    @part = @project.parts.create(params[:part])
    if @part.save
      redirect_to part_url(@part)
    else
      render :action => "new"
    end
  end

Tevens heb ik dit ook in de new() methode gedaan zodat het formulier ook meteen weet wat de relatie is. Thanks!

@Leftblank: Special thanks! Volgens mij heb ik het inzicht nu wel wat ik in het begin van dit topic nog niet had over de opzet van een rails applicatie! _/-\o_

Acties:
  • 0 Henk 'm!

  • djluc
  • Registratie: Oktober 2002
  • Laatst online: 19-09 16:12
Zit me af te vragen of dit mooi of not done is:
Ruby:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PartsController < ApplicationController
  before_filter :login_required, :defProject
  
  def defProject
    if params[:project_id]
      @project = Project.find(params[:project_id])
        
      if params[:id] 
        @part = @project.parts.find(params[:id])
      end
    else
      if params[:id] 
        @part = Part.find(params[:id])
        @project=@part.project
      end
    end
  end

Zo heb je altijd je part en je project beschikbaar. Altijd correct gerelateerd aan elkaar, ook al geef je geen project_id in de url mee. Zo kan je dus dus altijd er vanuit gaan dat beide objecten aanwezig zijn.

Acties:
  • 0 Henk 'm!

  • djluc
  • Registratie: Oktober 2002
  • Laatst online: 19-09 16:12
Ok, dit is dus duidelijk geen handige aanpak al ziet het er leuk uit. Waarom: Je kan meerdere records hebben die verwijzen naar elkaar. Dus project has_many parts en project has_many todo's. Dan werkt de create controller niet meer omdat deze reeds ingesteld is op de parts en niet ook nog op de todo's. Dus het script weet niet waar het heen moet.

Acties:
  • 0 Henk 'm!

  • Leftblank
  • Registratie: Juni 2004
  • Laatst online: 20:50
djluc schreef op zaterdag 16 januari 2010 @ 13:17:
Zit me af te vragen of dit mooi of not done is:
Ruby:
1
[..]

Zo heb je altijd je part en je project beschikbaar. Altijd correct gerelateerd aan elkaar, ook al geef je geen project_id in de url mee. Zo kan je dus dus altijd er vanuit gaan dat beide objecten aanwezig zijn.
Wat mij betreft zou 't kunnen, hetzij zoals je zelf net al stelde niet echt correct. Ook zou ik 't afraden om te kiezen voor je methode waarbij je altijd per se je project, task, part en notes beschikbaar wilt hebben; het is veel efficienter om puur te laden wat je nodig hebt, wanneer je het nodig hebt (dus per methode bekijken).

Nested resources werken inderdaad leuk, maar zijn vooral handig bij many-to-many relaties die je onder elkaar wil kunnen schikken (een task kan bij 2 projecten horen bijv, dan is 't nuttig om te weten welk project je 't nu over hebt). Verder zou ik het vooral weglaten wanneer mogelijk, en dus alleen bij acties als project/1/tasks/new uit de kast trekken.

Acties:
  • 0 Henk 'm!

  • djluc
  • Registratie: Oktober 2002
  • Laatst online: 19-09 16:12
Ik heb ze vrijwel nooit allemaal nodig inderdaad maar altijd wel de parent. Dus:

task -> note
user -> note

Dit omdat op die manier de associations werken in Rails. Dus ik doe: task.notes.new() bijvoorbeeld. Als ik dat echter inbouw kan ik niet ook nog user.notes.new() inbouwen, dat is het issue waar ik nu tegenaan loop.

Acties:
  • 0 Henk 'm!

  • djluc
  • Registratie: Oktober 2002
  • Laatst online: 19-09 16:12
Heeft iemand op mijn laatste vraag toevallig nog een leuk inzicht? Ik ben weer volop bezig met Ruby en het is toch erg vreemd dat iets 2 parents kan hebben wat van de code een rommeltje maakt.

Acties:
  • 0 Henk 'm!

  • dev10
  • Registratie: April 2005
  • Laatst online: 18-09 19:18
De term waar je op zoek naar bent is 'polymorphic association'. Voor meer informatie: http://railscasts.com/episodes/154-polymorphic-association

(Sowieso is die site een must voor iedere Ruby on Rails programmeur.)
Pagina: 1